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

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

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

我们介绍了 LiveData 的基本使用方式,包括 observe()MutableLiveDatasetValue()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 可以使用这个方法,诸位如果找不到这个方法,需要检查一下使用依赖库版本。

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

  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:

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 的情况。

例如用户姓名可能由 firstNamelastName 两个 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 进行监听。当 firstNameLiveDatalastNameLiveData 任何一个发生改变后,就会触发 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 项目依然十分重要。

相关推荐
天才少年曾牛1 小时前
Android新增服务添加selinux权限
android·java·frameworks
knighthood20011 小时前
ros2-quick-runner插件v0.0.4版本发布
android·java·开发语言
故渊at2 小时前
第五板块:Android 系统服务与电源管理 | 第十八篇:Battery Service 与 电量统计(Fuel Gauge)算法
android·算法·battery·电源·电池·电源管理·电量统计
2501_915909062 小时前
iOS IPA文件反编译与打包操作方法详解
android·ios·小程序·https·uni-app·iphone·webview
问心无愧051310 小时前
ctf show web入门111
android·前端·笔记
ha_lydms17 小时前
AnalyticDB分区、分布键性能优化
android·大数据·分布式·性能优化·分布式计算·分区·analyticdb
星辰17 小时前
Ijkplayer重新编译支持h264裸流
android
测试开发-学习笔记17 小时前
Android studio安装
android·ide·android studio
宋拾壹17 小时前
同时添加多个类目
android·开发语言·javascript