一、LiveData 的设计思想与优势
LiveData 这个名字,说白了就是「活的数据」。它不是一个简单的数据容器,而是一个能感知生命周期的数据持有者。我最早接触它的时候,心里想的是:「这不就是观察者模式套了个生命周期吗?」后来用深了才发现,事情没那么简单。
1.1 设计思想:生命周期感知
传统的观察者模式有个痛点——你得手动管理订阅和取消订阅。比如在 Activity 里注册了一个回调,如果忘了在 onDestroy 里取消,轻则内存泄漏,重则崩溃。LiveData 是怎么解决的呢?它内部维护了一个 LifecycleOwner 的弱引用,当 LifecycleOwner 处于活跃状态(STARTED 或 RESUMED)时,它才会通知观察者。一旦 LifecycleOwner 进入 DESTROYED 状态,LiveData 会自动清理观察者。
嗯,这里要注意:LiveData 并不是「不泄漏」,而是「自动清理」。它的核心思想是——数据应该随着页面的生命周期走,而不是反过来让页面去适配数据。
- 自动生命周期管理——不再需要手动取消注册
- 数据一致性——只在活跃状态下更新 UI,避免界面闪烁
- 无内存泄漏——Observer 会随着 LifecycleOwner 的销毁自动移除
- 共享资源安全——多个 Activity/Fragment 可以共享同一个 LiveData 实例
1.2 为什么不用普通的 Observable?
你想想看,如果用 RxJava 的 Observable,你得在 onPause 里 dispose,在 onResume 里重新订阅。代码写多了,难免有遗漏。我在项目中就遇到过这种情况——一个同事用 RxJava 监听网络状态,结果在 Fragment 销毁后还在更新 UI,直接报了 IllegalStateException。换成 LiveData 后,这种问题再也没出现过。
说白了,LiveData 就是为 Android 生命周期量身定做的。它不需要你操心「什么时候订阅、什么时候取消」,它自己会判断。
二、LiveData 的变换操作(map / switchMap)
实际开发中,我们拿到的数据往往不是 UI 直接需要的。比如从数据库查出来的是 User 对象,但界面上只需要显示用户名。这时候就需要对 LiveData 做变换。
2.1 map:一对一的同步变换
map 是最简单的变换。它接收一个 Function,把 LiveData 里的值转换成另一个值。注意,这个转换是同步的,不能做耗时操作。
val userLiveData: LiveData<User> = ...
val userNameLiveData: LiveData<String> = Transformations.map(userLiveData) { user ->
user.name
}
我在项目中经常用 map 来做数据脱敏。比如手机号只显示后四位,或者金额保留两位小数。这些操作不涉及异步,用 map 最合适。
2.2 switchMap:一对多的异步变换
switchMap 和 map 的区别在于:map 返回的是新值,而 switchMap 返回的是一个新的 LiveData。它常用于「根据一个 LiveData 的值,去触发另一个 LiveData 的查询」的场景。
举个例子:用户输入搜索关键词,你需要根据关键词去数据库查询结果。
val queryLiveData: LiveData<String> = ...
val searchResultLiveData: LiveData<List<Result>> = Transformations.switchMap(queryLiveData) { query ->
repository.search(query) // 返回 LiveData<List<Result>>
}
switchMap 有个关键特性——「切换」。当 queryLiveData 的值发生变化时,它会取消对上一个 LiveData 的订阅,转而订阅新的 LiveData。这避免了旧查询结果覆盖新结果的问题。我曾经在项目中踩过这个坑:用 map 去触发查询,结果两次查询的返回顺序不一致,导致 UI 显示错误。换成 switchMap 后,问题迎刃而解。
三、MediatorLiveData:多数据源合并
MediatorLiveData 是 LiveData 的增强版。它可以同时观察多个 LiveData 源,并在任何一个源发生变化时,触发自己的更新。说白了,它就是一个「数据合并器」。
3.1 典型场景:多个数据源合并
比如你的页面需要同时展示用户信息和系统通知。这两个数据来自不同的 LiveData,但 UI 需要等两个数据都准备好才能渲染。
val userLiveData: LiveData<User> = ...
val notificationLiveData: LiveData<Notification> = ...
val combinedLiveData = MediatorLiveData<CombinedData>().apply {
var user: User? = null
var notification: Notification? = null
fun update() {
val u = user ?: return
val n = notification ?: return
value = CombinedData(u, n)
}
addSource(userLiveData) { userData ->
user = userData
update()
}
addSource(notificationLiveData) { notif ->
notification = notif
update()
}
}
这个模式我用了很多次。它比用 LiveData 的 map 或者 switchMap 更灵活,因为你可以控制「什么时候才触发更新」。比如上面的例子,只有当两个数据都非空时,才发射合并后的值。
3.2 动态添加和移除数据源
MediatorLiveData 还支持动态添加和移除数据源。这在某些场景下非常有用,比如根据用户权限动态决定监听哪些数据。
val mediatorLiveData = MediatorLiveData<String>()
fun addSourceIfNeeded(source: LiveData<String>) {
if (!mediatorLiveData.hasActiveObservers()) {
mediatorLiveData.addSource(source) { value ->
mediatorLiveData.value = value
}
}
}
fun removeSource(source: LiveData<String>) {
mediatorLiveData.removeSource(source)
}
嗯,这里要注意:removeSource 之后,之前注册的 Observer 也会被移除。如果你只是暂时不想监听,可以考虑用 switchMap 配合一个开关 LiveData 来实现。
四、LiveData 在事件总线中的正确用法
很多人喜欢用 LiveData 做事件总线,比如 EventBus 的替代品。但这里有个大坑——LiveData 会「粘性」地发射数据。什么意思呢?就是当你注册一个 Observer 时,如果 LiveData 已经有值了,它会立刻把这个值发射给你。这在事件总线里往往是灾难。
4.1 粘性事件的问题
举个例子:你在 Activity A 里发送了一个「跳转到详情页」的事件,然后 Activity B 才注册监听。结果 Activity B 一启动,就收到了这个旧事件,直接跳转了。这显然不是你想要的行为。
我曾经在项目里用 LiveData 做全局的登录状态监听,结果每次 Fragment 重建时都会收到上一次的登录状态,导致 UI 闪烁。后来我改用 SingleLiveEvent 解决了这个问题。
4.2 解决方案:SingleLiveEvent
SingleLiveEvent 是一个自定义的 LiveData,它保证每个事件只被消费一次。实现原理很简单:在 setValue 时标记为未消费,在 observe 时只发射未消费的事件。
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner) { value ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(value)
}
}
}
override fun setValue(value: T?) {
pending.set(true)
super.setValue(value)
}
}
使用方式很简单:
val eventLiveData = SingleLiveEvent<String>()
// 发送事件
eventLiveData.value = "打开详情页"
// 接收事件(只会收到一次)
eventLiveData.observe(this) { event ->
// 处理事件
}
- 用 SingleLiveEvent 替代普通 LiveData 做事件总线
- 如果事件需要被多个 Observer 消费,考虑用 SharedFlow 替代
- 不要在 ViewModel 外部暴露 MutableLiveData,用 LiveData 暴露只读版本
4.3 更现代的方案:SharedFlow
如果你用的是 Kotlin 协程,SharedFlow 是比 LiveData 更合适的事件总线方案。它天然支持「只发射给新订阅者」的模式,而且没有生命周期绑定的限制。不过,如果你的项目还在用 LiveData 体系,SingleLiveEvent 已经足够用了。
我个人习惯是:UI 层的数据展示用 LiveData,跨页面的事件传递用 SharedFlow。这样既利用了 LiveData 的生命周期优势,又避免了粘性事件的问题。
五、知识体系总览
下面这张图总结了 LiveData 的核心知识点和它们之间的关系。你可以把它当作一个快速索引,遇到问题的时候回来看看。
这张图把 LiveData 的核心知识点分成了四个部分:设计思想、变换操作、多源合并和事件总线。每个部分之间都有联系,但各自独立。你在实际项目中,可以根据需求选择使用哪个部分。
好了,这一章的内容就到这里。LiveData 虽然看起来简单,但用好了能省很多事。下一章我们会聊聊 ViewModel 和 LiveData 的配合使用,到时候再深入讲讲它们之间的数据流设计。
公众号:蓝海资料掘金营,微信deep3321