The #1 mistake people do when using Cloud Firestore’s Android SDK
A must know if you’re using the Cloud Firestore SDK
Introduction
Hello everyone✋! Hope you’re doing well ❤️! In this short article I will describe the most common mistake that people do when they initially start using the Cloud Firestore SDK for Android.
Pre-requisites
- Conceptual understanding of the Cloud Firestore database
- Basic knowledge of using the Firestore SDK in Android
Preface
Before starting with anything, I’d like to say that the examples given below might seem unrealistic. In a production app, all network calls would be made in the data layer and the data layer would not be directly responsible for updating the UI. But in this article, all Firestore related calls will be assumed to be made in the viewmodel, which absolutely should not be done in a production app. The aim of this article is to demonstrate the mistake that people often do, and not to show the best architectural practices of building an app.
Guess what’s wrong!
Let’s say that you’re trying to write a document to Firestore, and you’re managing the UI state based on whether the write completes successfully or not. You also set the UI state to loading until this request completes. The code for that might look something like this.
class TestDialogViewModel : ViewModel() {
fun someFunction() {
val userDetails = hashMapOf(....)
uiState = UiState.Loading // set ui state to loading
Firebase.firestore
.document(".../...")
.set(userDetails)
.addOnSuccessListener {
uiState = UiState.Success // set ui state to success
}
.addOnFailureListener {
uiState = UiState.Failure // set ui state to failure
}
}
}
This is a very common pattern that people do when they initially start using the Cloud Firestore SDK. In the above code, the UI state is initially set to loading before the request is sent to Firestore. The UI state remains loading until the write either succeeds or fails. Stop for a moment, and think about what might be the issue with the code above.
Explanation
The above code snippet will work perfectly as long as the device is connected to an active internet connection. If you try to run it offline, you will notice that the UI state would be stuck in the loading state, even though the document gets “written”! (I’m intentionally surrounding the word with double-quotes.)
Let’s assume that a realtime listener is also attached to the collection to which the document is getting added to, in the viewmodel’s init block.
class TestDialogViewModel: ViewModel() {
.
.
init {
// WARNING: Listeners should always be removed when they are
// no longer needed. I'm not showing that here because this article
// is not about demonstrating how to use the Firebase SDK.
Firebase.firestore
.collection()
.addSnapshotListener {...} // listener for getting realtime updates
}
.
.
fun someFunction() {...}
}
Here’s the interesting bit. You’ll notice that the snapshot listener would get invoked with the document that was just “written” to the collection. So why is the UI state not getting updated even though the write was successfully made? Shouldn’t the onSuccessListener{}
be called?
Firebase.firestore
.document(".../...")
.set(userDetails)
.addOnSuccessListener {
// why isn't this not getting called?
uiState = UiState.Success
}
.addOnFailureListener {
uiState = UiState.Failure
}
Very confusing isn’t it? The reason for this weird behavior is straightforward.
A callback to a document write doesn’t get called until the write has actually been confirmed on the server.
There you go. That’s the reason why our callback wasn’t executed. Since the device was offline, the write was made locally in our cache (This is why I used the word “written” in double-quotes. It’s actually not written to the collection in the sever, the change is just stored locally. The update will be made, once the device gets back online). This also explains why the snapshot listener was triggered, but not the onSucessListener
.
The snapshot listener was triggered because, according to the snaptshot listener, an item was added to the collection. It doesn't care about whether the item was locally added in the offline cache, or it was actually written to the server. But, the onSuccessListener
on the other hand, will get triggered only when the server acknowledges the write. Since the server didn't acknowledge the write, the onSuccessListener
wasn’t triggered.
The solution
This is a weird behavior in my opinion and I hope it gets changed in the future. But, the recommended solution to this problem is to just remove the code that is in the addOnSuccessListener
block, and call it immediately. Code that is inside the onSuccessListener
block should only be reserved for code that needs to be executed when the write has been explicitly confirmed by the server. I know that this is weird, but this is the recommended solution from the Firebase team. Keeping this in mind, let’s fix our viewmodel’s logic.
class TestDialogViewModel: ViewModel() {
fun someFunction() {
val userDetails = hashMapOf(....)
uiState = UiState.WaitingToBeWrittenToServer //
Firebase.firestore
.document(".../...")
.set(userDetails)
.addOnSuccessListener {
uiState = UiState.SuccessfullyWrittenToServer //
}
.addOnFailureListener {
uiState = UiState.FailedToWriteToServer //
}
}
}
Again, this example might look a bit unrealistic, but, as I said before, the main aim of this article is just to demonstrate the mistake that people often commit when using the SDK.
Conclusion
And that wraps up this short blog post🎉 🎉! Hopefully, you found this blog post helpful! As always, I would like to thank you for taking the time to read this article😊. I wish you the best of luck! Happy coding 👨💻! Go create some awesome Android apps 👨💻! Cheers!
If you really liked my article and want to support me, you can do so, by clicking this link. Thank you so much for being generous ❤️, it really motivates me to keep going, and it helps me to keep my articles free for anyone to read. If you don’t feel like supporting, that’s fine too! The fact that you took some time off your schedule to read my article means a lot to me. Thank you 🙂
Some of my other articles that you might be interested in: