In this post, I will describe how I use the MVVM pattern with Firebase. This post assumes you are familiar with the MVVM architecture and have some experience with LiveData and coroutines. All code is written in Kotlin.
If you have read about MVVM with Android, the below diagram is what you are most likely familiar with.
However, with Cloud Firestore, we can remove the last two parts of it. This is because Firestore provides its own local cache. An (additional) local cache is not needed or recommended. This means we can remove the model and remote data source and combine them in a single repository class as shown in the below diagram.
For any given part of the app, I end up using four different classes:
- A single firestore service object
- Data class (Object representation of the required data)
Let’s start bottom-up, my data class is a standard Kotlin data class with a companion function to convert document snapshots to the profile object. I know the standard
.toObject(class) function can help me with this, but I prefer writing my own function for this. This helps better handle errors and provide default values. In some cases, it even helps shift from standard data types to enums for better representing certain elements.
A standard user profile data class can look something like:
You will notice the added Crashlytics reporting. This is something I found useful after working on a production database where data types were often mixed up. Further, by returning
null, I can tell my Firebase service function that something was wrong with this particular document and we can ignore it if needed.
Firestore Service object
For hosting our database code, we use a Kotlin object. This makes our Firebase Service a singleton, making only a single instance of this service available. This means we can easily access the functions defined here from anywhere in our code.
Side-note: As Rodrigo pointed out in the comments, be sure to include this dependency in your app level
build.gradle file. This is required to support coroutine calls like
await() for firebase objects.
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.1’ # Update to the latest version
A simple Firebase service object with a get function could be something like:
I am using suspend functions as they can easily be launched from any coroutine. They provide the flexibility of synchronous programming with async code. We will be calling this function from our view model which already has a coroutine scope
Notice that I am again using try-catch blocks. This time, it helps determine errors like missing documents/invalid collection names. Further, it makes use of the
toUser() extension function we wrote earlier. This makes the code both easier to read and more reusable.
Now, let’s say we need to get a collection of documents – for example a list of friends. You can add another function to the same object to return a list of users.
mapNotNull function? It is really useful in these situations. You want to avoid showing malformed documents and our original
toUser function returns a null for every document not confirming to our structure. We simply return an empty list in case we face some errors. The user doesn’t need to know what exactly went wrong. Only you need that information.
Another topic I would like to touch on is Kotlin flows. Flows are kind of like LiveData but don’t run until someone is collecting the flow. Then they emit values to the collector. Flow also adheres to Coroutine cancellation principles and can be extended with operators for complex tasks.
In Firestore terms, a flow can be used with a snapshot listener. We can attach a listener when the flow is first collected and then detach it when the flow is cancelled. This can be really useful for things like live feeds, chat applications, and apps where some data is being continuously updated.
Note that callback flow, which we are using below, is a part of the experimental coroutines API. So you will need to use the annotation
@ExperimentalCoroutinesApi whenever you use this.
A very simplified example of a flow is:
Please note that the above example is very basic. An actual use case can be much more complex.
Anyways, the main element to note here is the CallbackFlow. A callback flow is used (as the name suggests) to attach a flow to a callback like the Firebase snapshot listener. The basic steps being done here are:
- Creating a listener registration inside a callback flow
- Cancelling the registration in case of any error
- Emitting the results via the
- Calling awaitClose
awaitClose needs a special mention here. This single statement keeps this flow active and ensures it waits till it’s closed or cancelled. When it’s closed, we can safely detach the Firebase listener we attached earlier.
Now comes one of the most important elements of the architecture – The
ViewModel. The view model can be thought of as the heart of your app. It connects the data to the UI while handling the logical part of your app.
You need to calculate and update a score based on some formula? The view model handles it; You want to generate a random number? Leave it to the view model. This is also the layer where we convert the data from Firebase into our beloved LiveData streams and let the UI handle the rest.
Let’s create a view model for using the above repository we created:
Many interesting points to note here. Firstly, notice how I am using a private
_userProfile and a
userProfile differently. The major reason for this is that you don’t want to expose your mutable properties to your activity/fragment. Only the viewModel has access to the mutable live data while only the immutable live data is exposed.
Now, in this example, I have two liveDatas –
userProfile is an example of a single object while posts is a list of objects. These represent the two types of calls you will mostly be making to Firebase. A single document and a collection.
viewModelScope, it is a special
CoroutineScope provided by Android for use in your view models. Since our Firebase functions are defined as suspend functions, you can only call them from within a coroutine scope or another suspend function. However, using
viewModelScope makes it easy to manage your data. All pending coroutines are automatically cancelled when the view model is destroyed. This helps prevent memory leaks and unnecessary network calls. You can learn more about cancellation in coroutines here.
If you need your list to be updated, there are two ways. The first one is using a flow with a listener (as shown above) and you can use the
.asLiveData() method to convert your flow to a liveData. The other is manually querying the database again and adding the new documents to your list. For this, you can take help from the paging library. Firebase allows you to set conditions such as
limit for your queries where you can pass the last document you received and receive the next N documents.
Normally, I would store a private mutable list in the viewModel along with the live data abstractions for this particular case. When I get the new documents, I can add them to my mutable list and then update my live data with the new list.
Activities & Fragments
Now we come to our final layer in this architecture – our UI layer.
You are already familiar with this layer. I will only be highlighting how to use our view model with this.
Our first step with our fragment or activity is to initialize the views and then get an instance of our view model using
ViewModelProvider. Note that although I am using data binding in the above example, it is not necessary to use data binding in your project. Using it, however, will make your life easier.
So coming to our view model, we have two LiveDatas we want to observe from our
Fragment. We attach a simple
Observer while specifying the owner as
viewLifecycleOwner. This allows our LiveData to be automatically managed by the fragment’s lifecycle. When our lifecycle is destroyed, we will automatically stop observing the liveDatas.
Now, in our observers, we only get the current copy of the data. So,
userProfile will have the updated user profile as fetched from Firebase (or null if nothing is fetched yet).
posts will have the latest copy of our
posts list. We can then send this data to our adapters or use it directly in our views. Just like you normally do. When the data is updated, the observer is called again so you can easily update the UI almost instantaneously. Make sure your observer is suitable for running multiple times even in succession.
Now our UI layer doesn’t need to worry about any implementation detail. This means if your backend logic changes, you can easily make changes to your app without worrying about modifying the UI.
Thank you so much for reading — I hope you learnt something.
Architecture components with Kotlin make it easy to manage your app. I have personally used most of the concepts demonstrated here in actual production apps. You can learn more about Jetpack Architecture components on the Guide to app architecture. If you are just getting started with view models, this Codelab might be a good starting point.
I must admit this article was highly opinionated and based on my experience dealing with Firebase and MVVM. You might have a different or better approach to handling some of the things I mentioned here. If so, please do leave a comment, I would love to hear more about it!
Full Article: Yashovardhan Dhanania @ Firebase Developers
Mobile App Development Best Practices – 04.10
iOS New and Deprecated APIs in iOS 17 Abstract Class vs. Protocol-Oriented Approach in Swift Comparing the Performance of the...
New and Deprecated APIs in iOS 17
In this video, I would like to share with you some things that were either deprecated or added in iOS...
Promova helps people with dyslexia learn languages
The new Promova feature comes just in time for National Dyslexia Awareness Month and is available on the platform for...
Notify – A simple note application with modern MVVM, Compose and Material3
Notify is a simple note application that is built with Modern Android development tools. This project showcases the Good implementation...
Mobile App Development Best Practices – 03.10
iOS MetaCodable – Supercharge Swift’s Codable implementations with macros meta-programming How to build a Tuist plugin and publish it using...
How to make and use BOM (Bill of Materials) dependencies in Android projects
By using a BOM dependency, you can avoid specifying the versions of each individual library in your app, and let...