这个系列我做了协程和Flow开发者的一系列文章的翻译,旨在了解当前协程、Flow、LiveData这样设计的原因,从设计者的角度,发现他们的问题,以及如何解决这些问题,pls enjoy it。

在使用Android架构组件时,LiveData是一个很好的工具。在我知道如何使用Transformations类之前,我一直在滥用LiveData,并产生了大量的烂代码。在使用LiveData和架构组件的几年中,我想我已经找到了一些好的做法和模式,我想与你分享。

The basics…

对LiveData进行转换是非常容易的,有一个名为Transformations的辅助类正是为了这个目的。这个类提供了三个静态方法:map、switchMap和distinctUntilChanged,这些方法将在下面解释。下面的所有例子都将使用下面的数据类,它代表了我们从数据库或后台API接收的一个Player数据。这个Player模型只有一个名字和分数字段,以方便举例,但在现实中,它将有更多的字段。

  1. data class Player(val name: String, val score: Int = 0, val ...)

map

将LiveDatain的值转换为另一个值。下面是一个简单的例子,说明如何使用它。

  1. val player: LiveData<Player> = ...
  2. val playerName: LiveData<String> =
  3. Transformations.map(player) { it.name }

switchMap

将一个LiveDatain的值转换为另一个LiveData。 switchMap的转换可能有点棘手,所以让我们从一个简单的例子开始。我们想为Player实现一个基本的搜索功能。每次搜索文本发生变化时,我们都想更新搜索结果。下面的代码显示了它是如何工作的。

  1. val searchQuery: LiveData<String> = ...
  2. fun getSearchResults(query: String): LiveData<List<Player>> = ...
  3. val searchResults: LiveData<List<Player>> =
  4. Transformations.switchMap(searchQuery) { getSearchResults(it) }

distinctUntilChanged

对LiveData进行过滤,除非数值发生了变化,否则不会被检索出来。很多时候,我们可能会收到一个不包含任何相关变化的通知。如果我们监听的是所有球员的名字,我们不想在分数发生变化时更新用户界面。这就是distinctUntilChanged方法的用处。

  1. val players: LiveData<List<Player>> = ...
  2. val playerNames: LiveData<List<String>> =
  3. Transformations.distinctUntilChanged(
  4. Transformations.map(players) { players -> players.map { it.name } }
  5. )

这是一个非常好的功能,我在我的代码中经常使用它。对于我的使用情况,它主要与RecyclerView/适配器的更新有关。

livedata-ktx extensions for Transformations

上述所有的Transformations类函数也可以作为LiveData的扩展函数,使用下面的依赖。

  1. androidx.lifecycle:lifecycle-livedata-ktx:<version>

有了它,例如,你可以把上面的例子改写成下面这样。

  1. val players: LiveData<List<Player>> = ...
  2. val playerNames: LiveData<List<String>> = players.map { it.map { player -> player.name } }
  3. .distinctUntilChanged()

Behind the scenes of the Transformations class

我们刚刚涵盖了3个简单的转换,你实际上可以自己写。所有这些都是使用MediatorLiveData类编写的。MediatorLiveData类是我在处理LiveData时使用最多的类(尽管我在有意义的时候使用map / switchMap / distinctUntilChanged)。

为了给你一个例子,说明你什么时候应该创建你自己的MediatorLiveData类,看看这段代码。

  1. val players: LiveData<List<Player>> = ...
  2. val dbGame: LiveData<GameEntity> = ...
  3. val game: LiveData<Game> =
  4. Transformations.map(dbGame) { game ->
  5. val players = this.players.value // Getting current players here may be unsafe
  6. Game(players = game.playerIds.mapNotNull { playerId ->
  7. players?.find { it.id == playerId }
  8. })
  9. }

通过只映射dbGame的变化,我在Player更新时取了玩家的当前值(this.player.value)。所以,当Player被更新时,我并没有更新Game。为了解决这个问题,我应该使用MediatorLiveData来合并Player和Game,如果他们中的任何一个被更新。这将看起来像这样。

  1. val players: LiveData<List<Player>> = ...
  2. val dbGame: LiveData<GameEntity> = ...
  3. val game: LiveData<Game> = MediatorLiveData<Game>()
  4. .apply {
  5. fun update() {
  6. val players = players.value ?: return
  7. val game = dbGame.value ?: return
  8. value = Game(players = game.playerIds
  9. .mapNotNull { playerId ->
  10. players?.find { it.id == playerId }
  11. }
  12. )
  13. }
  14. addSource(players) { update() }
  15. addSource(dbGame) { update() }
  16. update()
  17. }

有了这个解决方案,每当球员或dbGame更新时,我都会得到Game更新。

MediatorLiveData

MediatorLiveData可以转换、过滤和合并其他LiveData实例。每当我创建MediatorLiveData时,我倾向于遵循同样的模式,它看起来像这样。

  1. val a = MutableLiveData<Int>(40)
  2. val b = MutableLiveData<Int>(2)
  3. val sum: LiveData<Int> = MediatorLiveData<Int>().apply {
  4. fun update() {
  5. // OPTION 3
  6. val aVal = a.value ?: return
  7. val bVal = b.value ?: return
  8. // OPTION 4
  9. value = aVal + bVal
  10. }
  11. // OPTION 1
  12. addSource(a) { update() }
  13. addSource(b) { update() }
  14. // OPTION 2
  15. update()
  16. }

在这个例子中,我正在观察两个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://github.com/ptornhult/livedata-utils/blob/master/app/src/main/java/se/codeunlimited/livedata/utils/LiveData.kt

向大家推荐下我的网站 https://xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问

【译】一篇掌握LiveData transformations - 图1