3、LiveData 与数据驱动:LiveData的工作原理,粘性事件问题,以及如何用MediatorLiveData合并多个数据源。
好,咱们今天来聊聊 LiveData。说实话,这个组件在 Jetpack 里算是比较“老资格”的了,但很多人用着用着就掉坑里了。尤其是那个粘性事件,我当年刚接触的时候也被坑过好几次。
LiveData 的核心思想其实很简单——数据驱动。你想想看,传统的做法是我们要主动去拉数据,比如 findViewById 然后 setText。但 LiveData 反过来,数据变了,它主动通知你。这种思想在 MVVM 架构里特别重要。
LiveData 的工作原理
LiveData 本质上是一个观察者模式的实现。但它比普通的观察者多了几个关键特性:
- 生命周期感知:它会自动感知 Activity 或 Fragment 的生命周期状态
- 自动取消订阅:当组件销毁时,自动移除观察者,避免内存泄漏
- 数据更新通知:数据变化时,只通知处于活跃状态的观察者
它的工作流程大致是这样的:
- 调用
observe()方法注册观察者,同时传入 LifecycleOwner - LiveData 内部维护一个 SafeIterableMap 来存储观察者
- 当数据变化时,遍历观察者列表,只通知状态为 STARTED 或 RESUMED 的观察者
- 当 LifecycleOwner 销毁时,自动移除对应的观察者
核心源码片段(简化版):
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
// 1. 检查是否在主线程
// 2. 包装成 LifecycleBoundObserver
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
// 3. 添加到观察者列表
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
// 4. 绑定生命周期
owner.getLifecycle().addObserver(wrapper);
}
这里有个细节要注意:observe() 方法必须在主线程调用。为什么?因为 LiveData 的设计初衷就是保证数据更新的线程安全性。我个人习惯在 ViewModel 里用 postValue() 在子线程更新数据,它会自动切回主线程通知。
粘性事件问题
嗯,说到这个我就来劲了。粘性事件,说白了就是:当你注册观察者时,如果 LiveData 已经有数据了,它会立刻把当前数据发给你。
为什么会这样?我们来看源码:
// LifecycleBoundObserver 中的关键逻辑
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
// 销毁时自动移除
mOwner.getLifecycle().removeObserver(this);
return;
}
// 关键在这里:状态变为活跃时,会触发一次数据分发
activeStateChanged(isActiveState(mOwner.getLifecycle().getCurrentState()));
}
当 LifecycleOwner 从非活跃变为活跃状态时,LiveData 会检查当前是否有数据,如果有,就立即发送。这就是粘性事件的根源。
我曾经踩过的坑:
有一次做消息通知功能,用户从通知栏点击进入详情页。我在 ViewModel 里用 LiveData 存了一个消息 ID,然后在 Activity 的 onCreate 里注册观察者。结果每次进入页面,都会触发一次旧消息的展示。这就是典型的粘性事件问题。
解决粘性事件,有几种常见方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 使用 Event 包装类 | 简单直接,一次性消费 | 需要手动处理消费状态 |
| 使用 SingleLiveEvent | 只发送一次,自动处理 | 非官方组件,需要自己实现 |
| 使用 Kotlin Flow + Channel | 更灵活,支持背压 | 学习成本高 |
我个人比较推荐第一种方案,写一个简单的 Event 包装类:
public class Event<T> {
private T mContent;
private boolean hasBeenHandled = false;
public Event(T content) {
mContent = content;
}
public T getContentIfNotHandled() {
if (hasBeenHandled) {
return null;
}
hasBeenHandled = true;
return mContent;
}
public boolean hasBeenHandled() {
return hasBeenHandled;
}
}
使用时,在 Observer 里调用 getContentIfNotHandled(),只有第一次会返回数据,后续都是 null。这样粘性事件就被“消费”掉了。
MediatorLiveData 合并多个数据源
MediatorLiveData 是 LiveData 的一个子类,它的作用就是合并多个 LiveData 源。你想想看,实际项目中经常需要同时监听多个数据变化,比如用户信息、网络状态、本地缓存,然后统一更新 UI。
它的用法很简单:
public class MyViewModel extends ViewModel {
private MediatorLiveData<String> mergedLiveData = new MediatorLiveData<>();
private LiveData<String> userLiveData = ...;
private LiveData<String> networkLiveData = ...;
public MyViewModel() {
// 添加第一个源
mergedLiveData.addSource(userLiveData, userInfo -> {
// 当 userLiveData 变化时,合并处理
String current = mergedLiveData.getValue();
mergedLiveData.setValue(combineData(current, userInfo));
});
// 添加第二个源
mergedLiveData.addSource(networkLiveData, networkStatus -> {
String current = mergedLiveData.getValue();
mergedLiveData.setValue(combineData(current, networkStatus));
});
}
private String combineData(String current, String newData) {
// 这里做你的合并逻辑
return current + " | " + newData;
}
public LiveData<String> getMergedLiveData() {
return mergedLiveData;
}
}
小技巧:
使用 MediatorLiveData 时,记得在适当的时机调用 removeSource()。比如你的 Fragment 进入后台时,可以暂时移除某些数据源,避免不必要的计算。我一般在 onPause() 里做这件事。
MediatorLiveData 还有一个妙用——解决数据竞争问题。比如你有两个数据源,一个来自网络,一个来自本地数据库。网络数据可能比本地数据晚到,但 UI 需要展示最终结果。用 MediatorLiveData 可以优雅地处理这种场景:
// 先展示本地数据
mergedLiveData.addSource(localData, data -> {
mergedLiveData.setValue(data);
});
// 网络数据到达后覆盖
mergedLiveData.addSource(networkData, data -> {
mergedLiveData.setValue(data);
// 网络数据已到,移除本地源
mergedLiveData.removeSource(localData);
});
这样既保证了 UI 的即时响应,又能在网络数据到达后更新为最新值。
总结一下
LiveData 虽然简单,但用好它需要理解它的生命周期感知机制和粘性事件特性。MediatorLiveData 则是一个强大的工具,能帮你优雅地处理多数据源合并的场景。
最后提醒一句:不要滥用 LiveData。如果你的数据流比较复杂,或者需要处理背压、重试等场景,建议考虑 Kotlin Flow。LiveData 最适合的场景是简单的 UI 数据绑定,尤其是和 DataBinding 配合使用时,简直绝配。
好了,这一章就到这里。下一章我们来聊聊 Room,看看数据库操作怎么和 LiveData 无缝衔接。