
LiveData transformations
LiveData
is a great tool to use when working with Android Architecture Components. Before I knew how to use the Transformations
class, I was abusing LiveData
and creating a lot of code-smell. Having worked with LiveData
and Architecture components for a few years now I think I’ve figured out some good practices and patterns that I’d like to share with you.
The basics…
Transforming LiveData is pretty easy to do and there is an awesome helper class named Transformations for just this purpose. This class provides three static methods: map
, switchMap
and distinctUntilChanged
which will be explained below. All of the examples below will use the following data class, which represents a player we receive from our database or backend API. This Player
model just has a name
and score
field to make it easier for the examples, but in reality, it would have many more fields.
map
Transforms the value of LiveData
into another value. Here’s a simple example of how this can be used:
switchMap
Transforms the value of a LiveData
into another LiveData
. switchMap
transformations can be a bit tricky, so let’s start with a simple example: We want to implement a basic search feature for players. Every time the search text changes we want to update search results. The following code shows how that works.
distinctUntilChanged
Filters LiveData
so that values will not be emitted unless they have changed. Many times we might be notified about a change that doesn’t contain any relevant changes. If we’re listening for the names of all players, we don’t want to update the UI when the score changes. This is where the distinctUntilChanged
method is useful.
This is a really neat feature and I tend to use it quite often in my code. For my use-cases, it’s mostly related to RecyclerView
/ adapter
updates.
livedata-ktx extensions for Transformations
All of the Transformations class functions above are also available as extension functions on LiveData using the dependency:
androidx.lifecycle:lifecycle-livedata-ktx:<version>
With this you can, for instance, rewrite the example above as:
Behind the scenes of the Transformations class
We’ve just covered 3 simple transformations that you can actually write your self. All of them are written using the MediatorLiveData
class. The MediatorLiveData
class is the class I probably use most of all when dealing with LiveData
(though I use map
/ switchMap
/ distinctUntilChanged
when it makes sense).
To give you an example of when you should be creating your own MediatorLiveData
class, look at this code:
By only mapping dbGame
changes, I’m taking the current value of players (this.players.value) when the game is updated. So, I’m not updating the game when players are updated. To solve this I should instead use MediatorLiveData
to merge players and games if any of them are updated. This would look something like this:
With this solution, I’m getting game updates whenever either players
or dbGame
is updated.
MediatorLiveData
MediatorLiveData
can transform, filter and merge other LiveData
instances. I tend to follow the same pattern whenever I create a MediatorLiveData
, which looks something like this:
In this example, I’m observing two LiveData sources (a and b). I’m calling the update
function when the mediator is created which will only emit a value if both sources are currently non-null. This pattern is very versatile, but let’s go through each step, one by one:
OPTION 1
What are the sources you want to monitor for changes before emitting anything from this LiveData
. This can be just a single source (or more) but has no fixed upper limit. (I.e. lets you do a conditional map of a single LiveData or merge multiple LiveDatas)
OPTION 2
Call the inner update
function here if you want to set an initial value when the MediatorLiveData
is created. For simplicity's sake, I usually call my update
function, but just setting the value
/postValue
of the MediatorLiveData
also works. In some cases, I don’t want to emit an initial value as I’m expecting this to emit null if a
or b
isn’t yet set. I then just skip calling update
or setting an initial value here.
OPTION 3
Since update
will be called whenever either a or b emits an update we must expect a and b to be null. Sometimes you actually want to update your MediatorLiveData
even though one or more sources are currently null, but this is a nice way of making sure the local variables aVal
and bVal
are non-null before emitting new values from the MediatorLiveData
. You can even apply more validation/filtering here to reduce the emission of the final MediatorLiveData
you’re creating.
OPTION 4
Since the MediatorLiveData
is a LiveData
instance we can either set the value
(like in the example above) or call postValue
(if for some reason you’re not on the main thread when emitting values). This is also where you decide how you want to transform the source data values. The example above just sums aVal
and bVal
, but you can of course apply any transformation you want here.
Conclusions
Use map, switchMap and distinctUntilChanged
for all LiveData transformations. Avoid writing your own transformations unless necessary and try to combine operations to create more complex ones.
Use distinctUntilChanged
to avoid emitting identical data, which causes unnecessary UI updates.
If you find yourself getting the current value of another LiveData
using the .value
property inside a map / switchMap
or inside an observe
-block, you should consider creating a MediatorLiveData
to merge the sources properly instead.
Bonus
Now that you know how MediatorLiveData
works you can go ahead and write your own amazing transformations. To get you started, here are some common extensions I’ve found or created for my own projects: