LiveData beyond the ViewModel — Reactive patterns using Transformations and MediatorLiveData
Reactive architecture has been a hot topic in Android for years now. It’s been a constant theme at Android conferences, normally illustrated with RxJava examples (see the Rx section at the bottom). Reactive programming is a paradigm concerned with how data flows and the propagation of change, which can simplify building apps and displaying data that comes from asynchronous operations.
One tool to implement some of the reactive concepts is LiveData. It’s a simple observable that is aware of the lifecycle of the observers. Exposing LiveData from your data sources or a repository is a simple way to make your architecture more reactive but there are some potential pitfalls.
This blog post will help you avoid traps and use some patterns to help you build a more reactive architecture using LiveData.
In Android, activities, fragments and views can be destroyed at almost any time, so any reference to one of these components can cause a leak or a
LiveData was designed to implement the observer pattern, allowing communication between the View controller (activities, fragments, etc.) and the source of the UI data (usually a ViewModel). With LiveData, this communication is safer: the data will only be received by the View if it’s active, thanks to its lifecycle awareness.
The advantage, in short, is that you don’t need to manually cancel subscriptions between View and ViewModel.
LiveData beyond the ViewModel
The observable paradigm works really well between the View controller and the ViewModel, so you can use it to observe other components of your app and take advantage of lifecycle awareness. For example:
- Observe changes in SharedPreferences
- Observe a document or collection in Firestore
- Observe the current user with an Authentication SDK like FirebaseAuth
- Observe a query in Room (which supports LiveData out of the box)
The advantage of this paradigm is that because everything is wired together, the UI is updated automatically when the data changes.
The disadvantage is that LiveData does not come with a toolkit for combining streams of data or managing threads, like Rx does.
Using LiveData in every layer of a typical app would look something like this:
In order to pass data between components we need a way to map and combine. MediatorLiveData is used for this in combination with the helpers in the Transformations class:
Note that when your View is destroyed, you don’t need to tear down these subscriptions because the lifecycle of the View is propagated downstream to the subsequent subscriptions.
One-to-one static transformation — map
In our example above, the ViewModel is only forwarding the data from the repository into the view, converting it to the UI model. Whenever the repository has new data, the ViewModel will simply have to
This transformation is very simple. However, if the user is subject to change, you need switchMap:
One-to-one dynamic transformation — switchMap
Consider this example: you are observing a user manager that exposes a user and you need to wait for their ID before you can start observing the repository.
You can’t wire this on initialization of the ViewModel because the user ID won’t be immediately available.
You can implement this with a
switchMap uses a MediatorLiveData internally, so it’s important to be familiar with it because you need to use it when you want to combine multiple sources of LiveData:
One-to-many dependency — MediatorLiveData
MediatorLiveData lets you add one or multiple sources of data to a single LiveData observable.
This example, from the docs, updates the result when any of the sources change. Note that the data is not combined for you. MediatorLiveData simply takes care of notifications.
In order to implement the transformation in our sample app, we need to combine two different LiveDatas into one:
A way to use MediatorLiveData to combine data is to add the sources and set the value in a different method:
The actual combination of data is done in the
It checks if the values are ready or correct and emits a result (loading, error or success)
See the bonus section below to learn how to clean this up with Kotlin’s extension functions.
When not to use LiveData
Even if you want to “go reactive” you need to understand the advantages before adding LiveData everywhere. If a component of your app has no connection to the UI, it probably doesn’t need LiveData.
For example, a user manager in your app listens to changes in your auth provider (such as Firebase Auth) and uploads a unique token to your server.
The token uploader can observe the user manager, but with whose Lifecycle? This operation is not related to the View at all. Moreover, if the View is destroyed, the user token might not ever be uploaded.
Another option is to use observeForever() from the token uploader and somehow hook into the user manager’s lifecycle to remove the subscription when done.
However, you don’t need to make everything observable. Let the user manager call the token uploader directly (or whatever makes sense in your architecture).
If part of your app doesn’t affect the UI, you probably don’t need LiveData.
Antipattern: Sharing instances of LiveData
When a class exposes a LiveData to other classes, think carefully if you want to expose the same LiveData instance or different ones.
If this class is a singleton in your app (there’s only one instance of it), you can always return the same LiveData, right? Not necessarily: there might be multiple consumers of this class.
For example, consider this one:
And a second consumer also uses it:
The first consumer will receive an update with data belonging to user “2”.
Even if you think you are only using this class from one consumer, you might end up with bugs using this pattern. For example, when navigating from one instance of an activity to another, the new instance might receive data from the previous one for a moment. Remember that LiveData dispatches the latest value to a new observer. Also, activity transitions were introduced in Lollipop and they bring with them an interesting edge case: two activities in an active state. This means that there could be two instances of the only consumer of the LiveData and one of them will probably show the wrong data.
The solution to this problem is simply to return a new LiveData for each consumer.
Think carefully before sharing a LiveData instance across consumers.
MediatorLiveData smell: adding sources outside initialization
Using the observer pattern is safer than holding references to Views (what you would normally do in a MVP architecture). However, this doesn’t mean you can forget about leaks!
Consider this data source:
It simply returns a new LiveData with a random value after 500ms. There’s nothing wrong with it.
In the ViewModel, we need to expose a
randomNumber property that takes the number from the generator. Using a MediatorLiveData for this is not ideal because it requires you to add the source every time you need a new number:
If every time the user clicks on the button we add a source to a MediatorLiveData, the app works as intended. However, we’re leaking all previous LiveDatas which won’t be sending updates any more, so it’s a waste.
You could store a reference to the source and then remove it before adding a new one. (Spoiler: this is what
Transformations.switchMap does! See solution below.)
Instead of using MediatorLiveData, let’s try (and fail) to fix this with
Transformation smell: Transformations outside initialization
Using the previous example this would not work:
There’s an important problem to understand here: Transformations create a new LiveData when called (both
switchMap). In this example
randomNumber is exposed to the View but it’s reassigned every time the user clicks on the button. It’s very common to miss that an observer will only receive updates to the LiveData assigned to the var in the moment of the subscription.
This subscription happens in
onCreate() so if the
viewmodel.randomNumber LiveData instance changes afterwards, the observer will never be called again.
In other words:
Don’t use Livedata in a var. Wire transformations on initialization.
Solution: wire transformations during initialization
Initialize the exposed LiveData as a transformation:
Use an Event in a LiveData to indicate when to request a new number:
See this post on events if you’re not familiar with this pattern.
Tidying up with Kotlin
The MediatorLiveData example above shows some code repetition so we can leverage Kotlin’s extension functions:
The repository looks much cleaner now:
LiveData and RxJava
Finally, let’s address the elephant in the room. LiveData was designed to allow the View observe the ViewModel. Definitely use it for this! Even if you already use Rx, you can communicate both with LiveDataReactiveStreams*.
If you want to use LiveData beyond the presentation layer, you might find that MediatorLiveData does not have a toolkit to combine and operate on streams of data like RxJava offers. However, Rx comes with a steep learning curve. A combination of LiveData transformations (and Kotlin magic) might be enough for your case but if you (and your team) already invested in learning RxJava, you probably don’t need LiveData.
*If you use auto-dispose, using LiveData for this would be redundant.