Flutter Cloud Firestore CRUD
Cloud Firestore is a database that lives on Google's servers and syncs with your app in real time. Add a record on one phone, and every other phone that watches the same data sees the change within moments. The service caches data on the device, so apps keep working during short offline periods.
How Firestore Organizes Data
Firestore does not use tables. It stores documents inside collections. A document holds fields, much like a JSON object. A collection holds many documents. Picture a filing cabinet: each drawer is a collection, and each folder inside is a document.
users (collection)
├── uid_101 (document)
│ ├── name: "Asha"
│ └── age: 28
└── uid_102 (document)
├── name: "Ravi"
└── age: 31
notes (collection)
└── note_a (document)
├── title: "Groceries"
├── body: "Milk, eggs"
└── ownerId: "uid_101"
Setting Up
Connect the app to Firebase as shown in the authentication topic, create a Firestore database in the console, and install the package:
flutter pub add cloud_firestore
final db = FirebaseFirestore.instance;
final notes = db.collection('notes');
Create
The add method creates a document with an automatic id. The set method creates a document with an id you choose.
Future<void> addNote(String title, String body, String ownerId) async {
await notes.add({
'title': title,
'body': body,
'ownerId': ownerId,
'createdAt': FieldValue.serverTimestamp(),
});
}
The serverTimestamp value asks the server to stamp the exact time, which avoids wrong phone clocks.
Read Once
Future<List<Map<String, dynamic>>> loadNotes() async {
final snapshot = await notes.orderBy('createdAt', descending: true).get();
return snapshot.docs.map((doc) {
final data = doc.data();
data['id'] = doc.id;
return data;
}).toList();
}
Read Live with a Stream
The snapshots() method returns a stream that fires whenever the data changes.
StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
stream: notes.orderBy('createdAt', descending: true).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
final docs = snapshot.data!.docs;
return ListView.builder(
itemCount: docs.length,
itemBuilder: (context, index) {
final note = docs[index].data();
return ListTile(
title: Text(note['title'] ?? ''),
subtitle: Text(note['body'] ?? ''),
);
},
);
},
)
Phone A adds a note ──→ Firestore ──→ snapshots() on Phone B
│
▼
StreamBuilder redraws the list
Update
The update method changes selected fields and leaves the others untouched.
Future<void> editNote(String id, String newTitle) async {
await notes.doc(id).update({'title': newTitle});
}
Use set with SetOptions(merge: true) to create the document when it does not exist yet.
Delete
Future<void> deleteNote(String id) async {
await notes.doc(id).delete();
}
Queries
Filter and sort data on the server.
final myNotes = await notes
.where('ownerId', isEqualTo: currentUid)
.orderBy('createdAt', descending: true)
.limit(20)
.get();
Some queries that combine filters with sorting need an index. Firestore prints an error with a link that creates the index in one click.
Using Typed Models
Convert documents into model objects with fromMap and toMap methods, as in the SQLite topic. Typed models catch spelling mistakes in field names at compile time.
Security Rules
Rules decide who may read and write. A database left open to everyone invites misuse. This rule lets each user read and write only their own notes:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /notes/{noteId} {
allow read, write: if request.auth != null
&& request.auth.uid == resource.data.ownerId;
}
}
}
Test rules with the emulator or the rules playground before release. Rules are the only real protection, since app code can be modified by attackers.
Watching the Cost
Firestore bills by the number of reads, writes, and deletes. A live list that reloads thousands of documents on every change adds up quickly. Limit results with limit(), paginate long lists, and avoid listening to entire collections when the screen shows only a few items.
Practice Ideas
Build a shared shopping list that updates live on two devices. Add a delete swipe and a checkbox that updates a done field. Write a security rule that limits each list to its owner, and confirm that a second user cannot read it.
Designing Your Data Shape
Firestore rewards planning around how the app reads data, not around how a spreadsheet would store it. A screen that shows a list of orders with the customer name should read from a single document that already contains the name, even if that means copying the name into each order. This copying is called denormalization, and it trades a little extra storage for fast, cheap reads. Decide the screens first, then design the documents that feed them.
Working Offline
Firestore keeps a local cache on mobile devices. Writes made offline appear in the app at once and sync to the server when the connection returns. Users keep working through a tunnel or an elevator ride. Show a small "syncing" hint when snapshot.metadata.hasPendingWrites is true, so people understand that their change has not reached the server yet.
