本文是 LiveData 的第二篇文章,在上一篇文章中:
Jetpack 可观察数据容器 LiveData 的入门与基础使用
我们介绍了 LiveData 的基本使用方式,包括 observe()、MutableLiveData、setValue()、postValue() 以及生命周期感知机制。简单来说,就是下面的这段代码:
kotlin
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。为了解释此方法的用途,我们得举个例子从一个实际需求开始。
假如一个用户的信息有下面的字段:
kotlin
data class User(
val id: Long,
val name: String,
val age: Int
)
基于这个数据类,我们就有了对应的 LiveData:
kotlin
val userLiveData = MutableLiveData<User>()
现在,为了在界面上展示用户的名字,我们就必须使用如下的代码:
kotlin
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:
kotlin
val userNameLiveData = userLiveData.map(
transform = { user: User ->
user.name
}
)
这里为了展示这个方法的真实面目,我使用了最原始的方法调用。map() 方法需要传入一个 lambda,该 lambda 从源数据的类型转换到目标的类型,在这个例子中就是从 User 类型转换为 String 类型。
有了这个新的 LiveData,那么在使用时我们就可以使用这个新的只监听用户名的 LiveData,以避免上面说的第一个问题:
kotlin
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() 方法即可:
kotlin
val userNameLiveData = userLiveData.map(
transform = { user: User ->
user.name
}
).distinctUntilChanged() // 仅在 user.name 发生改变时此 liveData 才会触发 onChanged
不过注意这里的 distinctUntilChanged() 方法仅在最新的 lifecycle-livedata-ktx 依赖库中有,我用的 2.10.0 可以使用这个方法,诸位如果找不到这个方法,需要检查一下使用依赖库版本。
有人看到这里就能发现这就是一个观察者链,没错,这里我们就捋一下观察者的流程:
- 创建 userLiveData
- 使用
map()创建 userNameLiveData - 修改 userLiveData 的数据后,会通知其观察者列表,其中包括 userNameLiveData
- userNameLiveData 收到回调后,从源数据 User 转换到 String,并设置到自己内部的 mData 中
- 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:
kotlin
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() 来做那么就是:
kotlin
val userLiveData = userIdLiveData.map( // userLiveData 的类型是 LiveData<LiveData<User>>
transform = { userId: Long ->
val userRepository = UserRepository()
userRepository.getUser(userId)
}
)
看似很正常,但是你会发现一个重要的问题:userLiveData 的类型是 LiveData<LiveData<User>>,这种类型是没有意义的,我们需要的是 LiveData<User>。
面对这种情况,我们就需要使用 switchMap(),将上面的代码做如下修改,就得到了正确的类型。
kotlin
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 的情况。
例如用户姓名可能由 firstName 与 lastName 两个 LiveData 共同决定。面对这种需求,LiveData 提供了一个特殊的实现:MediatorLiveData。
kotlin
val firstNameLiveData = MutableLiveData<String>()
val lastNameLiveData = MutableLiveData<String>()
为了将这两个 LiveData 合并为一个 LiveData,我们需要做如下的合并:
kotlin
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 进行监听。当 firstNameLiveData 和 lastNameLiveData 任何一个发生改变后,就会触发 fullNameLiveData 的改变,这就是 MediatorLiveData 的作用。
MediatorLiveData 本质上是一个可以同时观察多个 LiveData 的特殊 LiveData。它能够将多个数据源的变化汇总到同一个 LiveData 中,再统一对外分发。
observeForever()
前面我们说过使用 observe() 方法时,需要传入一个 LifecycleOwner,那么如果你没有 LifecycleOwner对象时,应该怎么办呢?Google 面对此情况特地提供了一个特殊的方法:observeForever()。它本质上就是一个不绑定生命周期的 observe():
kotlin
liveData.observeForever {
println(it)
}
如上面的方法调用,其不需要生命周期对象。它与 observe() 的区别只有一个:
observe()绑定生命周期,它能自动解绑;observeForever()需要手动解绑
而手动解绑只需要调用 removeObserver(),在使用此方法时,由于需要传递 Observer 对象,因此我们往往会使用这样的:
kotlin
// 创建观察者对象
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 项目依然十分重要。
