Keeping .observe() out of the ViewModel | Colin Dodd
If you've been using Android Architecture components you've probably got a nice separation of concerns. A View, doing everything UI; a Model, keeping control of your data sources; and a ViewModel, shuffling data between the two. LiveData makes it trivial to communicate between these layers, but how do you consume a LiveData in the ViewModel when you don't have a LifecycleOwner?
One of the big advantages of LiveData
is that they are lifecycle aware. When
you observe a LiveData
you send in a LifecycleOwner
and it ensures that the
observers are only informed of changes if they are in an active state. It's
great, you can observe and not have to worry about memory leaks, crashes, or
stale data.
It's trivial when observing a ViewModel's LiveData
in the View; but what about
the Model to the ViewModel? In my previous article I spoke about how you can use
NetworkBoundResource
in the Model to return cached database data whilst fetching fresh data from the
network. NetworkBoundResource
exposes its data through a LiveData
which
means the ViewModel needs to be able to observe it; but a ViewModel is not a
LifecycleOwner
nor does it have a reference to a LifecycleOwner
. So how can
a ViewModel observe these changes?
The wrong solution
There is a method called observeForever
- it will allow you to observe
without passing a LifecycleOwner
but it means the LiveData
is no longer
lifecycle aware. To stop observing you'll need to remember to call
removeObserver
.
I've made use of observerForever
in tests, but never in production code. I
would be interested in hearing under what circumstances observerForever
should
be used. When it comes to observing in the ViewModel, there is a better
solution.
The right solution
The
Transformations
class is what you need to keep observe
out of the ViewModel. With its map
and switchMap
methods you can consume LiveData
from the Model, and
transform that LiveData
into something which the View can observe.
Say you are developing an email app and you want to show the total number of unread messages. The Model only allows you to get a list of unread emails. The ViewModel however can transform the data from the Model into the unread counter that the View needs.
fun getNumberOfUnreadMessages(): LiveData<Integer> {
return Transformations.map(model.getUnreadMessages(), { it.size })
}
model.getUnreadMessages
is returning a LiveData<List<Email>>
which is being
mapped to the size of the list. This method returns a LiveData<Integer>
which
can be consumed by the View. No need to observe in the ViewModel.
switchMap
is useful if you need to send a parameter from the View to the
ViewModel. Imagine the situation where the user can search through their email
with a search query.
fun searchEmail(query: String): LiveData<List<Email>> {
return model.searchEmail(query)
}
The problem with the above code is that every time the View calls searchEmail
a different LiveData
will be returned, so the View will need to keep detaching
itself and attaching itself to the LiveData
returned from this function. By
using switchMap
we can ensure that the same LiveData
is returned and the
View doesn't need to do anything special at all.
private val userInputtedQuery = MutableLiveData<String>()
fun searchEmail(query: String) {
userInputtedQuery.value = query
}
val searchResult = Transformations.switchMap(userInputtedQuery, { model.searchEmail(it) })
By using switchMap
we can observe changes to one LiveData
, and trigger the
calling of another function. The LiveData
is transformed and returned to the
View where it can be observed; avoiding having to call observe in the ViewModel.
Conclusion
Make use of the
Transformations
class to keep observe out of your ViewModel. This keeps your LiveData
lifecycle aware and gives you all the benefits that entails.