这个系列我做了协程和Flow开发者的一系列文章的翻译,旨在了解当前协程、Flow、LiveData这样设计的原因,从设计者的角度,发现他们的问题,以及如何解决这些问题,pls enjoy it。
在使用Android架构组件时,LiveData是一个很好的工具。在我知道如何使用Transformations类之前,我一直在滥用LiveData,并产生了大量的烂代码。在使用LiveData和架构组件的几年中,我想我已经找到了一些好的做法和模式,我想与你分享。
The basics…
对LiveData进行转换是非常容易的,有一个名为Transformations的辅助类正是为了这个目的。这个类提供了三个静态方法:map、switchMap和distinctUntilChanged,这些方法将在下面解释。下面的所有例子都将使用下面的数据类,它代表了我们从数据库或后台API接收的一个Player数据。这个Player模型只有一个名字和分数字段,以方便举例,但在现实中,它将有更多的字段。
data class Player(val name: String, val score: Int = 0, val ...)
map
将LiveDatain的值转换为另一个值。下面是一个简单的例子,说明如何使用它。
val player: LiveData<Player> = ...
val playerName: LiveData<String> =
Transformations.map(player) { it.name }
switchMap
将一个LiveDatain的值转换为另一个LiveData。 switchMap的转换可能有点棘手,所以让我们从一个简单的例子开始。我们想为Player实现一个基本的搜索功能。每次搜索文本发生变化时,我们都想更新搜索结果。下面的代码显示了它是如何工作的。
val searchQuery: LiveData<String> = ...
fun getSearchResults(query: String): LiveData<List<Player>> = ...
val searchResults: LiveData<List<Player>> =
Transformations.switchMap(searchQuery) { getSearchResults(it) }
distinctUntilChanged
对LiveData进行过滤,除非数值发生了变化,否则不会被检索出来。很多时候,我们可能会收到一个不包含任何相关变化的通知。如果我们监听的是所有球员的名字,我们不想在分数发生变化时更新用户界面。这就是distinctUntilChanged方法的用处。
val players: LiveData<List<Player>> = ...
val playerNames: LiveData<List<String>> =
Transformations.distinctUntilChanged(
Transformations.map(players) { players -> players.map { it.name } }
)
这是一个非常好的功能,我在我的代码中经常使用它。对于我的使用情况,它主要与RecyclerView/适配器的更新有关。
livedata-ktx extensions for Transformations
上述所有的Transformations类函数也可以作为LiveData的扩展函数,使用下面的依赖。
androidx.lifecycle:lifecycle-livedata-ktx:<version>
有了它,例如,你可以把上面的例子改写成下面这样。
val players: LiveData<List<Player>> = ...
val playerNames: LiveData<List<String>> = players.map { it.map { player -> player.name } }
.distinctUntilChanged()
Behind the scenes of the Transformations class
我们刚刚涵盖了3个简单的转换,你实际上可以自己写。所有这些都是使用MediatorLiveData类编写的。MediatorLiveData类是我在处理LiveData时使用最多的类(尽管我在有意义的时候使用map / switchMap / distinctUntilChanged)。
为了给你一个例子,说明你什么时候应该创建你自己的MediatorLiveData类,看看这段代码。
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 }
})
}
通过只映射dbGame的变化,我在Player更新时取了玩家的当前值(this.player.value)。所以,当Player被更新时,我并没有更新Game。为了解决这个问题,我应该使用MediatorLiveData来合并Player和Game,如果他们中的任何一个被更新。这将看起来像这样。
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()
}
有了这个解决方案,每当球员或dbGame更新时,我都会得到Game更新。
MediatorLiveData
MediatorLiveData可以转换、过滤和合并其他LiveData实例。每当我创建MediatorLiveData时,我倾向于遵循同样的模式,它看起来像这样。
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()
}
在这个例子中,我正在观察两个LiveData源(a和b)。我在调解器创建时调用了更新函数,只有在两个源都是非空的情况下才会发出一个值。这种模式非常通用,但让我们一个一个地走完每一步。
方案1
在从这个LiveData发出任何东西之前,你想监控哪些源的变化。这可以只是一个单一的源(或更多),但没有固定的上限。(即让你对单个LiveData进行条件映射或合并多个LiveDatas)
方案2
如果你想在创建MediatorLiveData时设置一个初始值,在这里调用内部更新函数。为了简单起见,我通常调用我的更新函数,但只是设置MediatorLiveData的值/postValue也可以。在某些情况下,我不想发出一个初始值,因为我希望在a或b还没有设置的情况下发出空值。那么我就跳过在这里调用更新或设置初始值。
方案3
因为只要a或b发出更新,就会调用update,我们必须期望a和b为空。有时你实际上想更新你的MediatorLiveData,即使一个或多个来源目前是空的,但这是一个很好的方法,在从MediatorLiveData发出新值之前,确保局部变量aVal和bVal不是空的。你甚至可以在这里应用更多的验证/过滤,以减少你所创建的最终MediatorLiveData的排放。
方案4
由于MediatorLiveData是一个LiveData实例,我们可以设置值(像上面的例子)或调用postValue(如果由于某种原因,你在发射值时不在主线程上)。这也是你决定如何转换源数据值的地方。上面的例子只是将aVal和bVal相加,但你当然可以在这里应用你想要的任何转换。
结论
在所有的LiveData转换中使用map、switchMap和distinctUntilChanged。除非有必要,否则应避免编写自己的转换,并尝试结合操作来创建更复杂的转换。
使用distinctUntilChanged来避免发出相同的数据,这将导致不必要的UI更新。
如果你发现自己在地图/switchMap内或观察块内使用.value属性获得另一个LiveData的当前值,你应该考虑创建一个MediatorLiveData来正确合并来源。
原文链接:https://proandroiddev.com/livedata-transformations-4f120ac046fc
向大家推荐下我的网站 https://xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问