Build and publish a Flutter-based app for the App Store and Google Play — updated in 2026 to reflect the current Firebase and FlutterFire ecosystem.
TL:DR – The core appeal of Cloud Firestore hasn't changed: edit a document in the Firebase Console and you'll see it update on your device in real time. What has changed is how you wire everything together, with FlutterFire packages now fully stable, null safety baked in throughout, and the Firestore API meaningfully updated since the early days of this series.
Contents
What's changed in 2026
When this article was first written, FlutterFire was still maturing and the Firestore Flutter plugin had a number of rough edges — particularly around Android builds and type safety. Several things are worth flagging before you dive into the code below:
- The Firebase SDK (including Cloud Firestore) is now at version 12.14.0 on npm. The FlutterFire plugins have tracked these updates closely and the
cloud_firestorepackage is fully null-safe. - Cloud Firestore Enterprise edition in Native mode is now available, aimed at larger production workloads. For an indie app like this one, the standard tier is still the right choice, but it's worth knowing the platform has grown considerably.
- The old
Firestore.instanceAPI is deprecated. You now useFirebaseFirestore.instancefrom thecloud_firestorepackage. DocumentSnapshot.datais now a method call —snapshot.data()— not a property, and it returnsMap<String, dynamic>?, so null safety must be handled explicitly.- The
renamepackage on pub.dev has been updated and is more reliable than it was in earlier Flutter versions. It's still the recommended approach if you need to rename a project rather than starting fresh. - A Firestore Lite SDK is now available (via npm) for web use cases that only need simple REST-style reads and writes without real-time listeners — worth knowing if you later add a web target to this project.
The mock-data-first approach described below is still sound practice in 2026. Build against local data, verify the UI, then swap in the live Firestore stream. Nothing about that workflow has aged.
Mock data
Add the mock data just after void main() => runApp(MyApp()); in main.dart and modify the fields to match the Cloud Firestore collection you set up earlier. Once the app is working against this data, swapping in the live Firestore stream is a small change.
final dummySnapshot = [
{"name": "The Hand and Glove", "capacity": 15},
{"name": "The Purple Fountain", "capacity": 14},
{"name": "The Royal Acorn", "capacity": 11},
{"name": "Sparkle", "capacity": 10},
{"name": "Ration Town", "capacity": 1},
];
Add the rest of the page code at the bottom of main.dart, replacing the class that generated the old profile page. Update the route to match: '/Establishments': (context) => MyHomePage(), and update the onPressed action for the button that navigates to it: Navigator.pushNamed(context, '/Establishments');. The button with the Info icon should now load this mock data page.
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() {
return _MyHomePageState();
}
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Establishments')),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context) {
// TODO: replace with live Cloud Firestore stream
return _buildList(context, dummySnapshot);
}
Widget _buildList(BuildContext context, List snapshot) {
return ListView(
padding: const EdgeInsets.only(top: 20.0),
children: snapshot.map((data) => _buildListItem(context, data)).toList(),
);
}
Widget _buildListItem(BuildContext context, Map data) {
final record = Record.fromMap(data);
return Padding(
key: ValueKey(record.name),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(5.0),
),
child: ListTile(
title: Text(record.name),
trailing: Text(record.capacity.toString()),
onTap: () => print(record),
),
),
);
}
}
class Record {
final String name;
final int capacity;
final DocumentReference? reference;
Record.fromMap(Map<String, dynamic> map, {this.reference})
: assert(map['name'] != null),
assert(map['capacity'] != null),
name = map['name'],
capacity = map['capacity'];
Record.fromSnapshot(DocumentSnapshot snapshot)
: this.fromMap(
snapshot.data() as Map<String, dynamic>,
reference: snapshot.reference,
);
@override
String toString() => "Record<$name:$capacity>";
}
What did this code change?
Originally the app opened a second page when the Information action was pressed. Now it sets up mock data that will shortly be replaced by live Firestore documents. The _MyHomePageState replaces that second page with a new widget hierarchy — visible in the Flutter Inspector inside Android Studio or VS Code.

The AppBar title is set to Establishments. The body contains a ListView that renders the mock data, with each item displayed inside a rounded rectangle showing the name and capacity. The Record class is a lightweight data holder — not strictly required for a simple app, but it keeps the code readable and maps cleanly to the Firestore document structure.
Note the updated null-safety syntax compared to the original article: DocumentReference? is now nullable, and snapshot.data() is called as a method and cast explicitly. These changes are required in any current Flutter project.
Finished mock data page

Connect the Flutter app to Cloud Firestore
With the mock data confirmed working, replace _buildBody with a StreamBuilder that listens to the live Firestore collection. Note the updated API: FirebaseFirestore.instance replaces the old Firestore.instance, which is no longer valid.
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('establishments')
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const LinearProgressIndicator();
return _buildList(context, snapshot.data!.docs);
},
);
}
The StreamBuilder widget subscribes to the Firestore collection and rebuilds the list automatically whenever a document is added, updated, or removed. This is the real-time behaviour that makes Firestore compelling for this kind of app.
Update _buildList and _buildListItem to accept the typed Firestore snapshot:
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
return ListView(
padding: const EdgeInsets.only(top: 20.0),
children: snapshot.map((data) => _buildListItem(context, data)).toList(),
);
}
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final record = Record.fromSnapshot(data);
// ... rest of the method unchanged
}
If you hit a type mismatch between the capacity field — Firestore stores numbers as int or double depending on how the value was entered in the console — cast it explicitly in Record.fromMap:
capacity = (map['capacity'] as num).toInt();
This is cleaner than removing the type annotation entirely, which was the workaround in the original article.

Android runtime errors caused by mismatched applicationID
This tripped up the original build and it's still a common stumbling block, so it's worth keeping the warning here. If you've ever renamed your Flutter project's package name after the Firebase apps were registered, Android builds will fail with errors like:
Plugin project :firebase_core_web not found. Please update settings.gradle.
Plugin project :firebase_auth_web not found. Please update settings.gradle.
The root cause is that the applicationID, the Kotlin package name, the folder structure under src/main/kotlin/, and the name registered in the Firebase Console all need to agree. iOS tends to be more forgiving; Android is not.
The fix involves reconciling the package name across: app/build.gradle, the three AndroidManifest.xml files (debug, main, profile), MainActivity.kt, and the physical folder path under src/main/kotlin/. The rename package on pub.dev automates most of this and is more reliable in recent Flutter versions than it was when this series started. That said, if you're early in a project, the cleanest solution is still to start fresh with the correct name and copy your Dart source files across.
Don't rename your project mid-stream
Mentioned here purely as a caution. Renaming a Flutter project after Firebase integration is set up is entirely self-inflicted pain. Settle on your package name and Bundle ID before you register the app in the Firebase Console, and you'll never need this section.
It just works
Once the plumbing is correct, the experience is genuinely satisfying. Edit a document value in the Firebase Console, and the change appears on your device within a second or two — no refresh, no polling, no extra code.

Cloud Firestore scales automatically, has no servers to manage, and keeps your data in sync across client apps through real-time listeners.
The app now has a working skeleton: Material widgets, a live Firestore data stream, and real-time updates on both iOS and Android. The API calls are current, the null-safety patterns are correct for Flutter in 2026, and the architecture is solid enough to build on. Next up: making it actually useful.