In this series, I translated a series of articles of coroutines and Flow developers, aiming to understand the reasons for the current design of coroutines, flows and LiveData, find out their problems from the perspective of designers, and how to solve these problems. PLS Enjoy it.

LiveData is a great tool to use when working with Android architectural components. Until I knew how to use Mineralizations, I was abusing LiveData and producing a lot of bad code. Over the years of working with LiveData and architectural components, I think I’ve found some good practices and patterns that I’d like to share with you.

The basics…

Transformations to LiveData are easy, and there is a supplementary class called doubling just that. This class provides three static methods: Map, switchMap, and distinctUntilChanged, which are explained below. All of the following examples will use the following data class, which represents a Player data we receive from the database or background API. The Player model has only one name and score field for example purposes, but in reality it would have many more fields.

data class Player(val name: String, val score: Int = 0, val ...)
Copy the code

map

Converts the value of LiveDatain to another value. Here is a simple example of how to use it.

val player: LiveData<Player> = ...

val playerName: LiveData<String> = 
    Transformations.map(player) { it.name }
Copy the code

switchMap

Convert the value of one LiveDatain to another LiveData. The switchMap conversion can be a bit tricky, so let’s start with a simple example. We want to implement a basic search function for Player. We want to update our search results every time the search text changes. The following code shows how it works.

val searchQuery: LiveData<String> = ...

fun getSearchResults(query: String): LiveData<List<Player>> = ...

val searchResults: LiveData<List<Player>> = 
    Transformations.switchMap(searchQuery) { getSearchResults(it) }
Copy the code

distinctUntilChanged

LiveData is filtered and will not be retrieved unless the value changes. Many times, we may receive a notification that does not contain any relevant changes. If we’re listening to all the players’ names, we don’t want to update the user interface when the score changes. This is where the distinctUntilChanged method comes in.

val players: LiveData<List<Player>> = ...

val playerNames: LiveData<List<String>> = 
    Transformations.distinctUntilChanged(
        Transformations.map(players) { players -> players.map { it.name } }
    )
Copy the code

This is a very nice feature that I use a lot in my code. For my usage, it is mostly related to RecyclerView/ adapter updates.

livedata-ktx extensions for Transformations

All of the above functions can also be used as extension functions to LiveData, using the following dependencies.

androidx.lifecycle:lifecycle-livedata-ktx:<version>
Copy the code

With it, for example, you could rewrite the example above as follows.

val players: LiveData<List<Player>> = ...

val playerNames: LiveData<List<String>> = players.map { it.map { player -> player.name } }
        .distinctUntilChanged()
Copy the code

Behind the scenes of the Transformations class

We’ve just covered three simple transformations that you can actually write yourself. All of this is written using the MediatorLiveData class. The MediatorLiveData class is the one I use most when working with LiveData (although I use map/switchMap/distinctUntilChanged when it makes sense).

To give you an example of when you should create your own MediatorLiveData class, take a look at this code.

val players: LiveData<List<Player>> = ... val dbGame: LiveData<GameEntity> = ... val game: LiveData<Game> = Transformations.map(dbGame) { game -> val players = this.players.value // Getting current players here may be unsafe Game(players = game.playerIds.mapNotNull { playerId -> players? .find { it.id == playerId } }) }Copy the code

By mapping only dbGame changes, I take the current value of the Player (this.player.value) when the Player is updated. So, when the Player was updated, I didn’t update the Game. To solve this problem, I should use MediatorLiveData to merge Player and Game if either of them is updated. It’s going to look something like this.

val players: LiveData<List<Player>> = ... val dbGame: LiveData<GameEntity> = ... val game: LiveData<Game> = MediatorLiveData<Game>() .apply { fun update() { val players = players.value ? : return val game = dbGame.value ? : return value = Game(players = game.playerIds .mapNotNull { playerId -> players? .find { it.id == playerId } } ) } addSource(players) { update() } addSource(dbGame) { update() } update() }Copy the code

With this solution, I get a Game update every time a player or dbGame is updated.

MediatorLiveData

MediatorLiveData can transform, filter, and merge other LiveData instances. Whenever I create MediatorLiveData, I tend to follow the same pattern, which looks like this.

val a = MutableLiveData<Int>(40) val b = MutableLiveData<Int>(2) val sum: LiveData<Int> = MediatorLiveData<Int>().apply { fun update() { // OPTION 3 val aVal = a.value ? : return val bVal = b.value ? : return // OPTION 4 value = aVal + bVal } // OPTION 1 addSource(a) { update() } addSource(b) { update() } // OPTION 2 update() }Copy the code

In this example, I’m looking at two LiveData sources (A and B). I called the update function when the mediator was created and emitted a value only if both sources were non-empty. This model is very generic, but let’s take each step one at a time.

Plan 1

Which sources do you want to monitor for changes before issuing anything from this LiveData. This can be just a single source (or more), but there is no fixed upper limit. (i.e. lets you conditionally map a single LiveData or merge multiple LiveDatas)

Scheme 2

If you want to set an initial value when creating MediatorLiveData, call the internal update function here. For simplicity, I usually call my update function, but just setting the value of MediatorLiveData /postValue will do as well. In some cases, I don’t want to issue an initial value because I want to issue a null value if A or B hasn’t been set yet. So I’ll skip calling updates or setting initial values here.

Plan 3

Because update is called whenever either A or B issues an update, we must expect a and B to be null. Sometimes you actually want to update your MediatorLiveData, even if one or more sources are currently empty, but this is a good way to make sure local variables aVal and bVal are not empty before issuing new values from MediatorLiveData. You can even apply more validation/filtering here to reduce the emissions of the final MediatorLiveData you create.

Plan 4

Since MediatorLiveData is a LiveData instance, we can set the value (as in the example above) or call postValue (if for some reason you are not on the main thread when you emit the value). This is where you decide how to convert the source data values. The above example just adds aVal and bVal, but you can of course apply any transformations you want here.

conclusion

Use Map, switchMap, and distinctUntilChanged for all LiveData transformations. Avoid writing your own transformations unless necessary, and try to combine actions to create more complex transformations.

Use distinctUntilChanged to avoid emitting the same data, which would result in unnecessary UI updates.

If you find yourself using the.value property to get the current value of another LiveData within a map /switchMap or watch block, you should consider creating a MediatorLiveData to merge sources properly.

The original link: proandroiddev.com/livedata-tr…

Github.com/ptornhult/l…

I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit