Jetpack 可观察数据容器 LiveData 的高级用法

0 阅读3分钟

本文是 LiveData 的第二篇文章,在上一篇文章中:

Jetpack 可观察数据容器 LiveData 的入门与基础使用 - 掘金

我们介绍了 LiveData 的基本使用方式,包括 observe()MutableLiveDatasetValue()postValue() 以及生命周期感知机制。简单来说,就是下面的这段代码:

val liveData: MutableLiveData<Int> = MutableLiveData<Int>(0)
liveData.observe(lifecycleOwner, object : Observer<Int> {
    override fun onChanged(value: Int) {
        textView.text = "当前数值 : $value"
    }
})

不过在实际开发中,仅仅监听一个 LiveData 往往是不够的。很多时候我们需要对数据进行转换、根据条件切换数据源,甚至将多个 LiveData 合并成一个新的 LiveData。

为了解决这些问题,LiveData 提供了一系列高级 API。本文将介绍 map()switchMap()MediatorLiveData 以及 observeForever() 等高级用法。

LiveData.map

map() 方法将基于已有的一个 LiveData 创建一个新的 LiveData。为了解释此方法的用途,我们得举个例子从一个实际需求开始。

假如一个用户的信息有下面的字段:

data class User(
    val id: Long,
    val name: String,
    val age: Int
)

基于这个数据类,我们就有了对应的 LiveData:

val userLiveData = MutableLiveData<User>()

现在,为了在界面上展示用户的名字,我们就必须使用如下的代码:

userLiveData.observe(lifecyclerOwner, object: Observer<User> {
    override fun onChanged(value: User) {
        textView.text = "UserName: ${value.name}"
    }
})

这样的代码跑起来虽然能正常运行,但是不够优雅。之所以说不够优雅,主要是两个问题:

  • 使用 user.name 会导致界面层需要了解 User 结构。当数据类结构简单时还好,如果数据类相对复杂,那么界面上的维护就成了灾难。
  • 即使 user 中不关心的数据发生变化,也会导致 onChanged() 发生回调。例如当用户 age 属性发生变化,onChanged() 方法也会回调,这不仅没有意义,而且还浪费CPU资源。

面对这样的问题,最好的方式就是用 LiveData 的扩展方法 map(),此方法会基于源数据创建一个新的 LiveData:

val userNameLiveData = userLiveData.map(
    transform = { user: User ->
        user.name
    }
)

这里为了展示这个方法的真实面目,我使用了最原始的方法调用。map() 方法需要传入一个 lambda,该 lambda 从源数据的类型转换到目标的类型,在这个例子中就是从 User 类型转换为 String 类型。

有了这个新的 LiveData,那么在使用时我们就可以使用这个新的只监听用户名的 LiveData,以避免上面说的第一个问题:

userNameLiveData.observe(this, object: Observer<String> {
    override fun onChanged(value: String) {
        textView.text = "UserName: ${value}"
    }
})

特别注意,使用 map() 创建的 LiveData 并不能解决上面说的第二个问题,因为 map() 的职责是完成数据转换,而不是过滤重复值。因此即使转换后的结果没有发生变化,只要源 LiveData 发出了通知,map() 创建的新 LiveData 仍然会重新分发数据。在这里,即使你使用了 map() 创建一个只包含 userName 的 LiveData,但只要原始数据 User 发生改变,哪怕是你完全不关心的 age 发生变化,这个 map() 出来的只包含 userName 的 LiveData 也会发送通知,即回调 onChanged()

那么如果要解决第二个问题,应该怎么做呢?很简单,仅仅需要添加一个 distinctUntilChanged() 方法即可:

val userNameLiveData = userLiveData.map(
    transform = { user: User ->
        user.name
    }
).distinctUntilChanged()        // 仅在 user.name 发生改变时此 liveData 才会触发 onChanged

不过注意这里的 distinctUntilChanged() 方法仅在最新的 lifecycle-livedata-ktx 依赖库中有,我用的 2.10.0 可以使用这个方法,诸位如果找不到这个方法,需要检查一下使用依赖库版本。

有人看到这里就能发现这就是一个观察者链,没错,这里我们就捋一下观察者的流程:

  1. 创建 userLiveData
  2. 使用 map() 创建 userNameLiveData
  3. 修改 userLiveData 的数据后,会通知其观察者列表,其中包括 userNameLiveData
  4. userNameLiveData 收到回调后,从源数据 User 转换到 String,并设置到自己内部的 mData 中
  5. userNameLiveData 的内部数据发生改变,会通知其观察者列表,就是外部的使用方

总结一下,map() 创建的新 LiveData 并不会独立保存一份 User 数据,它仍然依赖于原始的 userLiveData。当 userLiveData 发生变化时,map() 会重新执行转换逻辑,并将转换后的结果分发给新的 LiveData。新的 LiveData 内部数据发生变化时,就会通知外部的使用方。

LiveData.switchMap

map() 适用于将一个数据转换为另一种数据,但如果转换的结果本身就是一个 LiveData,那么 map() 就无法满足需求了。因为如果 map() 的 transform 返回的结果是一个 LiveData<T>,那么再经过 map() 包装,最终返回的结果是个嵌套的 LiveData<LiveData<T>>,这不是我们想要的。此时就需要使用 LiveData 提供的另一个转换方法:switchMap()

我们先假设有这么一个 Respository:

class UserRepository {
    fun getUser(id: Long): LiveData<User> {
        return MutableLiveData(User(id = id, name = "Taylor", age = 20))
    }
}

现在,我们有一个包含 userId 的 LiveData,当这个 LiveData 发生改变时,我们将通过这个 UserRepository.getUser() 方法来获取 User,也就是要完成一个从 LiveData<userId>LiveData<User> 的转换。

如果我们从 map() 来做那么就是:

val userLiveData = userIdLiveData.map(        // userLiveData 的类型是 LiveData<LiveData<User>>
        transform = { userId: Long ->
            val userRepository = UserRepository()
            userRepository.getUser(userId)
        }
    )

看似很正常,但是你会发现一个重要的问题:userLiveData 的类型是 LiveData<LiveData<User>>,这种类型是没有意义的,我们需要的是 LiveData<User>

面对这种情况,我们就需要使用 switchMap(),将上面的代码做如下修改,就得到了正确的类型。

val userLiveData = userIdLiveData.switchMap(    // userLiveData 的类型是 LiveData<User>
    transform = { userId: Long ->
        val userRepository = UserRepository()
        userRepository.getUser(userId)
    }
)

可见,当你的转换代码如果返回的是 LiveData<T>,那么你就需要使用 switchMap() 方法了。事实上,switchMap 中的 switch 指的是“切换观察的数据源”。当 userIdLiveData 的值发生变化时,switchMap() 会重新执行 transform,并得到一个新的 LiveData。此时它会停止观察旧的 LiveData,转而开始观察新的 LiveData。因此无论 userId 如何变化,最终对外始终只暴露当前最新的数据源。

早期版本中通常通过 Transformations.map()Transformations.switchMap() 完成数据转换,而在引入 KTX 扩展后,更推荐使用 LiveData 的 map()switchMap() 扩展函数,两者本质上实现的是相同功能。因此如果你引用了新版本的 livedata 依赖库后,就只能使用扩展方法的 map()switchMap() 了。

MediatorLiveData

前面介绍的 map()switchMap() 都是基于一个 LiveData 创建另一个 LiveData。但在实际开发中,我们经常会遇到需要同时依赖多个 LiveData 的情况。

例如用户姓名可能由 firstNamelastName 两个 LiveData 共同决定。面对这种需求,LiveData 提供了一个特殊的实现:MediatorLiveData

val firstNameLiveData = MutableLiveData<String>()
val lastNameLiveData = MutableLiveData<String>()

为了将这两个 LiveData 合并为一个 LiveData,我们需要做如下的合并:

val fullNameLiveData = MediatorLiveData<String>()
fullNameLiveData.addSource(firstNameLiveData, object : Observer<String> {
    override fun onChanged(value: String) {
        fullNameLiveData.value = "$value ${lastNameLiveData.value}"
    }
})
fullNameLiveData.addSource(lastNameLiveData, object : Observer<String> {
    override fun onChanged(value: String) {
        fullNameLiveData.value = "${firstNameLiveData.value} $value"
    }
})

在这样处理之后,就可以使用 fullNameLiveData 进行监听。当 firstNameLiveDatalastNameLiveData 任何一个发生改变后,就会触发 fullNameLiveData 的改变,这就是 MediatorLiveData 的作用。

MediatorLiveData 本质上是一个可以同时观察多个 LiveData 的特殊 LiveData。它能够将多个数据源的变化汇总到同一个 LiveData 中,再统一对外分发。

observeForever()

前面我们说过使用 observe() 方法时,需要传入一个 LifecycleOwner,那么如果你没有 LifecycleOwner对象时,应该怎么办呢?Google 面对此情况特地提供了一个特殊的方法:observeForever()。它本质上就是一个不绑定生命周期的 observe()

liveData.observeForever {
    println(it)
}

如上面的方法调用,其不需要生命周期对象。它与 observe() 的区别只有一个:

  • observe() 绑定生命周期,它能自动解绑;observeForever() 需要手动解绑

而手动解绑只需要调用 removeObserver(),在使用此方法时,由于需要传递 Observer 对象,因此我们往往会使用这样的:

// 创建观察者对象
val observer = object : Observer<String> {
    override fun onChanged(value: String) {
        textView.text = value
    }
}
// 注册观察者
liveData.observeForever(observer)

// 移除观察者
liveData.removeObserver(observer)

由于 observeForever() 不受生命周期管理,因此在不再需要观察时必须主动调用 removeObserver()。如果忘记移除观察者,LiveData 将一直持有该 Observer,从而可能导致内存泄漏。

至此,LiveData 的核心功能已经全部介绍完毕。从最基础的 observe()MutableLiveData,到数据转换相关的 map()switchMap(),再到用于组合多个数据源的 MediatorLiveData,以及脱离生命周期管理的 observeForever(),这些基本覆盖了 LiveData 在实际开发中的主要使用场景。

不过随着 Kotlin 协程与 Flow 的普及,越来越多的新项目已经开始使用 StateFlow 替代 LiveData。但由于大量现有项目仍然基于 LiveData 构建,因此理解这些 API 对于阅读和维护 Android 项目依然十分重要。