9、LiveData与Event事件处理:单次事件问题、EventWrapper封装、避免事件重复消费
各位同学,今天我们来聊一个非常实际的问题——LiveData的事件处理。
说实话,LiveData本身是个好东西,但它在事件场景下有个“坑”。这个坑我踩过不止一次,后来才找到比较优雅的解法。今天我就把这段经验掰开揉碎了讲给你听。
9.1 单次事件问题:为什么LiveData不适合直接发事件?
先看一个典型场景:
你有一个ViewModel,里面有个LiveData<String>用来触发一个Snackbar提示。用户在Activity里观察它,收到数据后弹出提示。一切看起来很正常。
但问题来了——屏幕旋转后,Activity重建,LiveData会重新发射最后一次的值。于是Snackbar又弹出来了。用户什么都没做,却看到同样的提示。
这就是所谓的单次事件问题。
为什么会这样?
因为LiveData的设计初衷是状态持有,不是事件分发。状态需要持久化,事件只需要消费一次。你把事件当状态用,自然就会重复消费。
核心区别:状态是“当前是什么”,事件是“发生了什么”。
状态可以反复读,事件只能消费一次。
我在项目中遇到过这样一个案例:用户点击“提交订单”按钮,ViewModel里通过LiveData发射了一个“提交成功”事件。结果用户旋转了一下屏幕,事件又触发了,订单被重复提交了两次。嗯,那天的线上事故就是这么来的。
9.2 EventWrapper封装:一个优雅的解决方案
怎么解决?
思路其实很简单:给事件加一个“已消费”标记。每次观察者收到事件时,判断一下这个事件是否已经被消费过。如果消费过了,就忽略。
我习惯用一个叫EventWrapper的封装类来处理。代码长这样:
class EventWrapper<out T>(private val content: T) {
private var hasBeenHandled = false
/**
* 获取事件内容,仅第一次调用返回有效值
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* 直接查看事件内容,不改变消费状态
*/
fun peekContent(): T = content
}
用法也很简单:
// ViewModel 中
private val _snackbarEvent = MutableLiveData<EventWrapper<String>>()
val snackbarEvent: LiveData<EventWrapper<String>> = _snackbarEvent
fun showSnackbar(message: String) {
_snackbarEvent.value = EventWrapper(message)
}
// Activity 中
viewModel.snackbarEvent.observe(this) { eventWrapper ->
eventWrapper.getContentIfNotHandled()?.let { message ->
Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show()
}
}
你看,核心逻辑就一行:getContentIfNotHandled()。第一次调用返回内容,之后都返回null。屏幕旋转后,Activity重建,重新观察同一个LiveData,但hasBeenHandled已经是true了,所以事件不会重复触发。
小提示:peekContent()方法我一般只在调试时用,或者某些需要“看一眼但不消费”的场景。生产代码里尽量少用,容易破坏语义。
9.3 避免事件重复消费:更多实战细节
EventWrapper解决了核心问题,但实际项目中还有一些细节需要注意。
9.3.1 多个观察者的问题
如果一个LiveData有多个观察者,EventWrapper默认只让第一个观察者消费到事件。后面的观察者拿到的都是null。
这其实符合大多数场景——事件只需要被处理一次。但如果你需要多个观察者都收到事件,怎么办?
我建议换个思路:用多个LiveData,或者用SharedFlow。EventWrapper不适合做广播。
9.3.2 粘性事件 vs 非粘性事件
LiveData默认是粘性的——新观察者会立即收到最后一次的值。EventWrapper虽然能防止重复消费,但新观察者还是会收到一次null(因为事件已经被消费了)。
这其实没问题,因为null不会触发任何操作。但如果你觉得不舒服,可以用NonStickyLiveData或者SingleLiveEvent。不过我个人更推荐EventWrapper,因为它更通用,也更容易测试。
避坑指南:我曾经在项目里用过SingleLiveEvent,但后来发现它在多个Fragment共享同一个ViewModel时会有问题。EventWrapper配合普通的MutableLiveData,反而更稳定。
9.3.3 事件队列的场景
有些场景下,你需要连续发射多个事件。比如用户快速点击按钮,每个点击都要触发一个事件。
EventWrapper默认只保留最新的一个事件。如果你需要队列,可以用List<EventWrapper<T>>或者Channel。不过说实话,我很少遇到这种需求。大多数情况下,一个事件对应一次操作就够了。
9.4 知识体系总览
下面这张图帮你理清本章的核心逻辑:
9.5 总结
好了,我们来收个尾。
LiveData的事件处理,说白了就是一句话:别把事件当状态用。EventWrapper就是给事件加了个“一次性”的保险丝,烧过一次就断了,不会再触发。
我个人习惯在项目里统一用EventWrapper来包装所有的事件型LiveData。这样团队里所有人都遵循同一个模式,不容易出问题。
最后送你一个检查清单:
- ✅ 所有“一次性”事件都用EventWrapper包装
- ✅ 观察者里只用
getContentIfNotHandled()消费事件 - ✅ 避免一个LiveData被多个观察者同时消费事件
- ✅ 屏幕旋转后验证事件不会重复触发
做到这四点,事件重复消费的问题基本就跟你无缘了。
公众号:蓝海资料掘金营,微信deep3321