Waiting for Firestore to create a document
I recently saw this in a (Flutter) code-base:
final cred = await FirebaseAuth.instance.signInAnonymously();
final user = cred.user!;
Future.delayed(const Duration(seconds: 15);
final doc = await FirebaseFirestore.instance.doc(user.uid).getDoc();
I asked the engineer why there was a 15 second delay hard-coded in there, and it turns out there is a backend process (a Cloud Function in this case) that creates a document with (default) settings for the user, and they didn’t know how long that takes.
This sort of code leads to all kinds of problems. The 15 seconds is a pretty safe upper limit, but typically document creation will take a lot less time - and you’ll be waiting needlessly for the rest of the 15 seconds. And in the one case in a million where it takes more than 15 seconds to create the document, you’re gonna have a hard time debugging it.
By using Firestore’s built-in realtime listeners is actually quite easy to wait for the document to be created, but no longer…
The simplest way to see this is to actually use a listener, which (based on the documentation linked above) works like this:
final uid = FirebaseAuth.instance.currentUser!.uid;
final ref = FirebaseFirestore.instance.doc('users/${uid}')
ref.snapshots().listen((snapshot) {
if (snapshot.exists) {
debugPrint(snapshot.data());
}
});
With this you’ll see debug output right when the document is created. Firestore actively listens for the update on the database server, and gets notified (server-to-client) right away when that happens.
If you want to use this in your UI, you can hook it up in a StreamBuilder
like this:
StreamBuilder<DocumentSnapshot>(
stream: ref.snapshots(),
builder: (_, snapshot) {
if (snapshot.hasError) return Text('Error listening document: ${snapshot.error}');
if (!snapshot.hasData) return const CircularProgressIndicator();
final doc = snapshot.data!;
return Text('Current doc: ${doc.data()}');
}
)
But in our use-case above, we didn’t want to use the document in a UI. Rather we want to wait for the document to appear, so we want to use await
on it. And that’s not possible with (just) a realtime listener.
Instead we’ll write a helper function that translates the Stream
from the realtime listener into a Future
that we need for await
. Based on what we’ve seen so far with listen
, that’d be:
Future<DocumentSnapshot> waitForDocument(DocumentReference ref) {
final completer = new Completer<DocumentSnapshot>();
StreamSubscription? sub;
try {
sub = ref.snapshots().listen((snapshot) {
if (snapshot.exists) {
completer.complete(snapshot);
sub!.cancel();
}
});
}
catch (e) {
completer.completeError(e);
}
return completer.future;
}
It’s a bit longer than I wanted it to be, but it should be pretty straightforward to follow once you know that:
- We need to cancel the listener once the document has been created, so we track the subscription (in
sub
). - A
Completer
is an object that allows you to build your ownFuture
.
Now with this, we can wait for our document to appear without a hard-coded delay:
final cred = await FirebaseAuth.instance.signInAnonymously();
final user = cred.user!;
final doc = await waitForDocument(FirebaseFirestore.instance.doc(user.uid));
What’s not yet handled here is the case where document creation has not completed after 15 seconds. To add that, we’d need to implement a timeout. Let me know (on the socials I linked above) if that’s something you need.
You can find a working copy of this code and a (much shorter) explanation on https://zapp.run/edit/firestore-wait-for-doc-creation-zx5k06w7x5l0.
Update: Luke pointed out that the firstWhere
operation on Stream
that does almost the same. Using that, we can reduce waitForDocument
to just this:
Future<DocumentSnapshot> waitForDocument(DocumentReference ref) {
return ref.snapshots().firstWhere((snapshot) => snapshot.exists);
}
The calling code remains the same, and gets the same benefits - so this is just a much shorter implementation of waitForDocument
. 🎉