3、LiveData 与数据驱动:LiveData的工作原理,粘性事件问题,以及如何用MediatorLiveData合并多个数据源。

好,咱们今天来聊聊 LiveData。说实话,这个组件在 Jetpack 里算是比较“老资格”的了,但很多人用着用着就掉坑里了。尤其是那个粘性事件,我当年刚接触的时候也被坑过好几次。

LiveData 的核心思想其实很简单——数据驱动。你想想看,传统的做法是我们要主动去拉数据,比如 findViewById 然后 setText。但 LiveData 反过来,数据变了,它主动通知你。这种思想在 MVVM 架构里特别重要。

LiveData 的工作原理

LiveData 本质上是一个观察者模式的实现。但它比普通的观察者多了几个关键特性:

  • 生命周期感知:它会自动感知 Activity 或 Fragment 的生命周期状态
  • 自动取消订阅:当组件销毁时,自动移除观察者,避免内存泄漏
  • 数据更新通知:数据变化时,只通知处于活跃状态的观察者

它的工作流程大致是这样的:

  1. 调用 observe() 方法注册观察者,同时传入 LifecycleOwner
  2. LiveData 内部维护一个 SafeIterableMap 来存储观察者
  3. 当数据变化时,遍历观察者列表,只通知状态为 STARTED 或 RESUMED 的观察者
  4. 当 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 无缝衔接。