前言
最近读到一篇博文,一语惊醒梦中人。虽然早就注意到了这个问题的存在,却对解决方法没有任何的思考,看来我对MVVM的理解依旧不够深。十分感谢博主,学习到了很有用的知识,特记录此笔记。
MVVM中的“事件”
在MVVM架构中,ViewModel通常承担着处理数据的责任,在处理完数据后,将新的UI状态通过LiveData传递给View进行UI更新。其中,LiveData起到了在VM与V中的数据通信作用。
这个逻辑没有任何问题,同时也是目前MVVM的主流和推荐的做法。
然而,相信很多人在使用MVVM架构的过程中都遇到了这个问题:有些时候ViewModel处理完数据后,并不需要更新UI,只不过是想要进行一些需要Context的操作。也就是本文中的“事件(Event)”
例如:弹出Toast、导航、页面跳转。这些或许算UI“操作”,但在我的理解上和UI的“更新”八竿子打不着边。
这时候很容易想到用LiveData来传递……事件?
错错错,大错特错!
事件(Event)和状态(State)的区别
那篇博文关于这里讲的很好,这里简单摘抄一下:
虽然“状态”和“事件”都可以通过响应式的方式通知到 UI 侧,但是它们的消费场景不同:
-
状态(State):是需要 UI 长久呈现的内容,在新的状态到来之前呈现的内容保持不变。比如显示一个Loading框或是显示一组请求的数据集。
-
事件(Event):是需要 UI 即时执行的动作,是一个短期行为。比如显示一个 Toast 、 SnackBar,或者完成一次页面导航等。
我们从覆盖性、时效性、幂等性等三个维度列举状态和事件的具体区别
状态 | 事件 | |
---|---|---|
覆盖性 | 新状态会覆盖旧状态,如果短时间内发生多次状态更新,可以抛弃中间态只保留最新状态即可。这也是为什么 LiveData 连续 postValue 时会出现数据丢失。 | 新事件不应该覆盖旧事件,订阅者按照发送顺序接收到所有事件,中间的事件不能遗漏。 |
时效性 | 最新状态是需要长久保持的,可以被时刻访问到,因此状态一般是“粘性的”,在新的订阅出现时为其发送最新状态。 | 事件只能被消费一次,消费后应该丢弃。因此事件一般不是“粘性”的,避免多次消费。 |
幂等性 | 状态是幂等的,唯一状态决定唯一UI,同样的状态无需响应多次。因此 StateFlow 在 setValue 时会对新旧数据进行比较,避免重复发送。 | 订阅者需要对发送的每个事件进行消费,即使是同一类事件发送多次。 |
看到这里,你是不是也和我一样突然恍然大悟?
传递事件的正确姿势
EventBus、Flow都可以用于处理事件,然而历史悠久的EventBus虽然业界推崇但太难还有点小题大做、未来主流的Flow虽然谷歌推荐但太新不会用还有点看不懂。
就想用LiveData,怎么办?
这里贴一个相对完善的解决方案:
open class LiveEvent<T> : MediatorLiveData<T>() {
private val observers = ArraySet<ObserverWrapper<in T>>()
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
observers.find { it.observer === observer }?.let { _ -> // existing
return
}
val wrapper = ObserverWrapper(observer)
observers.add(wrapper)
super.observe(owner, wrapper)
}
@MainThread
override fun observeForever(observer: Observer<in T>) {
observers.find { it.observer === observer }?.let { _ -> // existing
return
}
val wrapper = ObserverWrapper(observer)
observers.add(wrapper)
super.observeForever(wrapper)
}
@MainThread
override fun removeObserver(observer: Observer<in T>) {
if (observer is ObserverWrapper && observers.remove(observer)) {
super.removeObserver(observer)
return
}
val iterator = observers.iterator()
while (iterator.hasNext()) {
val wrapper = iterator.next()
if (wrapper.observer == observer) {
iterator.remove()
super.removeObserver(wrapper)
break
}
}
}
@MainThread
override fun setValue(t: T?) {
observers.forEach { it.newValue() }
super.setValue(t)
}
private class ObserverWrapper<T>(val observer: Observer<T>) : Observer<T> {
private var pending = false
override fun onChanged(t: T?) {
if (pending) {
pending = false
observer.onChanged(t)
}
}
fun newValue() {
pending = true
}
}
}
该解决方案最早由 Jose Alcérreca 给出了思路和基本框架,经过了许多大佬修改和完善,使用方法也大致和LiveData相同。
有一点比较重要的是,LiveData数据是粘性的,无论何时订阅都会立即触发一次 onChanged()
。对于状态来说,订阅时立即获得最新的状态,这没有问题;然而对于事件来说,并不需要获得订阅前的上一个事件。
变量 pending
在这里正是用于解决这一问题的。在初始化时 pending = false
,使得在刚订阅时不会立即触发 onChanged()
。
相似解决方式有很多,例如美团和这篇文章的方案都是设置一个版本标识符,初始化时等于LiveData的version,再通过判断新接收的数据的version是否大于存储的version来分辨是不是上一次的数据。
另外,该文章中也提供了使用LiveData实现事件总线的思路和例子,可以参考。
不过我并不希望在 ObserverWrapper
中持有LiveData对象,因此推荐使用 pending
做法。
因此,实际上,这个方案与LiveData的区别只是非粘性而已,对 postValue
丢失数据的问题并没有进行处理。方法当然也是有的,我参考这篇文章对该方案进行了修改,增加了一部分内容以避免丢数据:
open class LiveEvent<T> : MediatorLiveData<T>() {
...
private var mainHandler: Handler? = null
override fun postValue(value: T) {
if (mainHandler == null) {
mainHandler = Handler(Looper.getMainLooper())
}
mainHandler!!.post(Runnable {
setValue(value)
})
}
...
}
那么,这一段代码为什么会生效呢?这个就要涉及到LiveData的源码了,可以看这篇文章,里面对这两个方法的源码进行的分析和解释。
简单来说,postValue
其实就是使用了一个线程锁(这也是 postValue
高并发时丢失数据的原因),使用Handler切换到主进程后调用setValue
来设置数据。
因此,我们只需要重写 postValue
,收到新数据一律直接抛到主线程 setValue
即可。
现在,我们已经有了一个简单好用、不会丢数据、非粘性、可感知生命周期、可观察的事件对象了。
最后,贴上总代码:
LiveEvent.kt
:
import android.os.Handler
import android.os.Looper
import androidx.annotation.MainThread
import androidx.collection.ArraySet
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Observer
import kotlinx.coroutines.Runnable
open class LiveEvent<T> : MediatorLiveData<T>() {
private val observers = ArraySet<ObserverWrapper<in T>>()
private var mainHandler: Handler? = null
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
observers.find { it.observer === observer }?.let { _ -> // existing
return
}
val wrapper = ObserverWrapper(observer)
observers.add(wrapper)
super.observe(owner, wrapper)
}
@MainThread
override fun observeForever(observer: Observer<in T>) {
observers.find { it.observer === observer }?.let { _ -> // existing
return
}
val wrapper = ObserverWrapper(observer)
observers.add(wrapper)
super.observeForever(wrapper)
}
@MainThread
override fun removeObserver(observer: Observer<in T>) {
if (observer is ObserverWrapper && observers.remove(observer)) {
super.removeObserver(observer)
return
}
val iterator = observers.iterator()
while (iterator.hasNext()) {
val wrapper = iterator.next()
if (wrapper.observer == observer) {
iterator.remove()
super.removeObserver(wrapper)
break
}
}
}
@MainThread
override fun setValue(value: T) {
observers.forEach { it.newValue() }
super.setValue(value)
}
override fun postValue(value: T) {
if (mainHandler == null) {
mainHandler = Handler(Looper.getMainLooper())
}
mainHandler!!.post(Runnable {
setValue(value)
})
}
private class ObserverWrapper<T>(val observer: Observer<T>) : Observer<T> {
private var pending = false
override fun onChanged(t: T?) {
if (pending) {
pending = false
observer.onChanged(t)
}
}
fun newValue() {
pending = true
}
}
}
当然,建议还是至少要了解一下被谷歌十分看重并当做未来主流来提供全方位支持的Flow,相比LiveData更贴合Kotlin的协程,非常强大。
关于读写权限
在MVVC架构中,LiveData应该只能由ViewModel持有和写入,而View层则应该只读,所以我们经常会有以下写法:
private val _name = MutableLiveData<String>()
val name: LiveData<String> = _name
fun somefunction(){
...
_name.postValue("失迹")
...
}
这个想法和写法并没有问题。将具有读写权限的MutableLiveData设为私有成员,只对View暴露只读的LiveData。
此写法更为规范,但仅仅是为了防止你在View和Observe中误修改,就需要增加代码的数量和降低阅读性,我个人是极不情愿的。
就如同约束性的问题一样,我们真的有必要为了防止可以避免的失误而增加多余且无实际作用的内容吗?
或许我们需要照顾不懂操作的用户,但我们有必要考虑其他不懂规范的程序员到这种程度吗?
话题扯远了。因为LiveEvent本质上就是个没有粘性、不会丢数据的LiveData,因此自然是支持以上写法的:
private val _showToast = LiveEvent<String>()
val showToast: LiveData<String> = _showToast
fun somefunction(){
...
_showToast.postValue("如你所见,这是一条吐司")
...
}
这样写的话最后在类型上又会转回LiveData,不过没有影响,当做事件用即可。
对于跳转页面、弹出Snacebar之类的事件我们无可奈何,但除此之外,我们应当避免其他的事件。将数据处理为UI状态,才是MVVM的正确实践!
附录
参考文献
-
Jose Alcérreca , LiveData with SnackBar, Navigation and other events
版权信息
本文原载于reincarnatey.net,遵循CC BY-NC-SA 4.0协议,复制请保留原文出处。