skip to content

Firestore & Storage Calls

cloud_firestore gives typed references via withConverter and live snapshots() streams over an offline cache; firebase_storage uploads files as observable UploadTasks. Interviewers probe listener cost.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

5

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?

level: juniorimportance: must knowfreq 58%

answer

  1. Future versus Stream
  2. initial event sent immediately
  3. server first, cache fallback
  4. hasPendingWrites on local writes
  5. a listener stays open until cancelled

basics

~20 s

get() 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 lines
dart
import '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

for a junior

Know that get() returns a Future with one snapshot and snapshots() returns a Stream that emits right away and on every change.

for a middle

Explain get()'s server-then-cache default, snapshots()' cache-first first event, and what hasPendingWrites and isFromCache report.

for a senior

Choose read versus listen per screen, avoid polling, and use snapshot metadata to show honest saved and offline states.

for a principal

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.
open as a page

With Flutter's firebase_storage plugin, how do you upload a photo with putFile, show progress, and obtain a download URL afterwards?

level: juniorimportance: should knowfreq 48%

basics

~10 s

Call ref.putFile(file) on a child Reference to get an UploadTask, listen to task.snapshotEvents for bytesTransferred and totalBytes, await the task (a Future<TaskSnapshot>), then call getDownloadURL() on the uploaded reference.

open as a page

In Flutter's cloud_firestore, how do Settings.persistenceEnabled and cacheSizeBytes shape offline behaviour, and when must those settings be applied?

level: middleimportance: should knowfreq 30%

basics

~10 s

persistenceEnabled chooses an on-disk or in-memory cache, and cacheSizeBytes sets the size that triggers cleanup of little-used data. Assign FirebaseFirestore.instance.settings before any other Firestore call; web uses memory unless persistence is enabled.

open as a page

In Flutter's cloud_firestore plugin, what does withConverter do on a collection or document reference, and why prefer typed references over raw maps?

level: middleimportance: should knowfreq 44%

basics

~10 s

withConverter<R>(fromFirestore:, toFirestore:) returns a reference typed to R, so reads yield model objects and writes accept them. The mapping runs client-side only; Firestore still stores ordinary map fields.

open as a page

In a Flutter app using cloud_firestore, why is calling snapshots() inside build() costly, and how do you keep Firestore listeners cheap?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Each snapshots() call creates a new stream whose first listen starts a native listener that re-runs the query. Calling it in build() restarts that listener on every rebuild; create the stream once, cancel manual subscriptions, and keep queries narrow.

open as a page