In Flutter's cloud_firestore plugin, what is the difference between get() and snapshots() on a DocumentReference or query, and when do you use each?
answer
- Future versus Stream
- initial event sent immediately
- server first, cache fallback
- hasPendingWrites on local writes
- a listener stays open until cancelled
basics
~20 sget() returns a Future with one snapshot, trying the server, then the cache. snapshots() returns a Stream that emits current data at once and on every change, local writes included. Use get() for one-off reads, snapshots() for live screens.
solid answer
~40 s`get()` reads once and returns `Future<DocumentSnapshot<T>>` or `Future<QuerySnapshot<T>>`; by default it tries the server and falls back to the cache, and `GetOptions(source: Source.server)` or `Source.cache` changes that. `snapshots()` returns a `Stream` that sends an initial event immediately and a new one whenever the data changes — local writes included, with `metadata.hasPendingWrites` set until the server confirms. With the default `ListenSource.defaultSource`, the first event can come from the cache, which `metadata.isFromCache` reveals. In a shared trip itinerary edited by several travellers you listen with `snapshots()`; for a single check, such as whether an invite code exists, `get()` is enough. A listener holds resources until its subscription is cancelled, so it is not a free substitute for a read.
code
dart · 14 linesimport 'package:cloud_firestore/cloud_firestore.dart';
final CollectionReference<Map<String, dynamic>> trips =
FirebaseFirestore.instance.collection('trips');
// One-off: does this invite code exist?
Future<bool> inviteExists(String code) async {
final snapshot = await trips.doc(code).get();
return snapshot.exists;
}
// Live: the shared itinerary for one trip.
Stream<QuerySnapshot<Map<String, dynamic>>> itinerary(String tripId) =>
trips.doc(tripId).collection('itinerary').orderBy('day').snapshots();go deeper
Know that get() returns a Future with one snapshot and snapshots() returns a Stream that emits right away and on every change.
Explain get()'s server-then-cache default, snapshots()' cache-first first event, and what hasPendingWrites and isFromCache report.
Choose read versus listen per screen, avoid polling, and use snapshot metadata to show honest saved and offline states.
Set a data-access rule for the app so listeners are reserved for screens that must stay live, keeping listener counts and read volume predictable.
## References first In the `cloud_firestore` plugin, nothing is read until you ask. `FirebaseFirestore.instance.collection('trips')` returns a `CollectionReference`, and `.doc('lisbon-2026')` returns a `DocumentReference` — cheap, local objects that only describe a path. A `CollectionReference` is also a `Query`, so filters and ordering produce further `Query` objects. Reading happens through one of two methods, and the choice between them is one of the first things a Flutter interview asks about Firestore. ## `get()` — one read `get([GetOptions? options])` returns a `Future` of a single snapshot: `Future<DocumentSnapshot<T>>` on a document reference, `Future<QuerySnapshot<T>>` on a query. The pinned doc comment describes its default precisely: it attempts to fetch from the server and **falls back to the cache**. `GetOptions` can instead force `Source.server` (fail when offline) or `Source.cache` (never touch the network). Use `get()` when the answer is needed once — checking whether a trip code exists before joining, or loading a document to prefill an edit form. ## `snapshots()` — a live listener `snapshots({bool includeMetadataChanges = false, ListenSource source = ListenSource.defaultSource})` returns a `Stream<DocumentSnapshot<T>>` or `Stream<QuerySnapshot<T>>`. The documented behaviour for a document is that **an initial event is sent immediately, and further events are sent whenever the document is modified**. For a query, each event carries the full current result set in `docs`, and `docChanges` lists what was added, modified or removed since the previous event. Two details matter in practice: - **Local writes show up at once.** When the user adds an item to the shared itinerary, the listener fires before the server confirms; that snapshot's `metadata.hasPendingWrites` is `true`. - **The first event may come from the cache.** With the default `ListenSource.defaultSource`, the listener tries to return an initial snapshot from the cache and then keeps up with the server; `metadata.isFromCache` tells you which. `ListenSource.cache` restricts the listener to the local cache entirely. ## Side by side | | `get()` | `snapshots()` | |---|---|---| | Return type | `Future` of one snapshot | `Stream` of snapshots | | When it answers | Once | Immediately, then on every change | | Offline default | Server, falling back to cache | Cache first, then server updates | | Sees other users' edits | Only if called again | Yes, as they happen | | Must be cancelled | No | Yes — it holds a listener open | ## Reading the snapshot objects - **`DocumentSnapshot<T>`** — `exists` says whether the document is there, `data()` returns `T?` (null when it does not exist), `id` is the document ID and `reference` points back to it. - **`QuerySnapshot<T>`** — `docs` is the list of `QueryDocumentSnapshot<T>` in query order, `size` counts them, and `docChanges` lists what changed since the previous snapshot. - **`metadata`** on both — a `SnapshotMetadata` with `hasPendingWrites` and `isFromCache`. A deleted document still produces a snapshot on a document listener: `exists` flips to `false` and `data()` returns `null`, so listener code must handle both. ## Choosing in a group-trip planner 1. **Itinerary screen** — several travellers edit the same day plan, so the screen listens with `snapshots()` on the trip's itinerary query. 2. **Join-a-trip dialog** — a single `get()` on the invite document is enough. 3. **Trip settings form** — `get()` to prefill, then a write on save; a live listener would overwrite what the user is typing. ## Common mistakes - **Polling with `get()` on a timer** to imitate live updates: more reads, more latency, and still stale between polls. - **Listening where a read would do**, and forgetting that a listener stays open until its subscription is cancelled. - **Assuming `get()` always hits the server.** Offline, the default quietly returns cached data; pass `GetOptions(source: Source.server)` when only fresh data will do. - **Ignoring metadata.** A "Saved" badge driven by the first local event is premature; `hasPendingWrites` becoming `false` is the moment the server has the write. ```dart import 'package:cloud_firestore/cloud_firestore.dart'; final DocumentReference<Map<String, dynamic>> trip = FirebaseFirestore.instance.collection('trips').doc('lisbon-2026'); Future<bool> tripExists() async => (await trip.get()).exists; Stream<List<String>> itineraryTitles() => trip .collection('itinerary') .orderBy('day') .snapshots() .map((s) => s.docs.map((d) => d.data()['title'] as String).toList()); ```
- How can a snapshots() listener tell that a displayed itinerary change has reached the server?The snapshot's `metadata.hasPendingWrites` is `true` while it reflects local writes not yet committed. With `includeMetadataChanges: true`, the listener receives another event when it turns `false`, which is the moment to show a saved indicator. Without that flag, metadata-only changes produce no event.
- What does get() return when the device is offline and the document was read before?With default `GetOptions`, `get()` tries the server and falls back to the cache, so it returns the cached copy and `metadata.isFromCache` is `true`. Pass `GetOptions(source: Source.server)` when the caller must fail rather than show stale data.
saying these in an interview costs you the question
- snapshots() waits for the first server change before emitting anything.
- get() always goes to the server and fails when the device is offline.
- Calling get() on a timer is an acceptable way to get live updates.
- Local writes only appear in snapshots() after the server confirms them.
- A snapshots() stream closes itself after the first event.