视图(Activity或Fragment)与ViewModel进行通信的一种便捷的方式是使用LiveData,视图可以订阅LiveData中的数据变化并对其作出反馈。这适用于那些需要一直在屏幕上显示的数据。
但是,有些数据只应该被消费一次,比如显示Snackbar消息、导航事件或者弹出对话框。
不要试图使用第三方库或者是扩展Architecture来解决这个问题,你应当把它看作是一个设计问题。We recommend you treat your events as part of your state。本文将列出一些解决这个问题的错误方法,并给出我们推荐的方法。
❌ Bad: 1. Using LiveData for events
这种方法在LiveData对象内部直接持有Snackbar消息或导航信号。原则上它看起来像一个普通的LiveData对象,但是它存在一些问题。
在一个列表/详情的应用中,列表界面的ViewModel如下:
// Do not use this for events
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.value = true
}
}
复制代码
在视图(Activity或Fragment)里:
listViewModel.navigateToDetails.observe(this, Observer {
if (it) startActivity(DetailsActivity...)
})
复制代码
这种方法的问题在于,_navigateToDetails的值会一直为true,导致无法返回到列表界面。详细情况如下:
- 用户在列表界面中点击按钮进入详情界面。
- 用户按下返回键返回到列表界面。
- 列表界面的观察者观察到_navigateToDetails的值为
true
,会再次错误的跳转到详情界面。
一种解决方法是在_navigateToDetails的值被设置为true
之后,立即修改它的值为flase
:
fun userClicksOnButton() {
_navigateToDetails.value = true
_navigateToDetails.value = false // Don't do this
}
复制代码
上面的方法是错误的。因为LiveData中虽然可以存储数据,但不保证发出它接收到的每个值。例如:在没有观察者处于活动状态时设置一个值,这时候如果再给它设置一个新的值,那么新的值将直接替换旧的值。此外,在不同线程中设置值可能会导致冲突,使得它只会向观察者发出一次变化通知。
但是这种方法的主要问题在于如何确保导航事件发生后LiveData中的值一定会被被重置?
❌ Better: 2. Using LiveData for events, resetting event values in observer
上面的方法还可以衍生出另一种解决方法:在视图中告诉ViewModel你已经处理了该导航事件,并且希望它重置该事件,即修改_navigateToDetails的值为flase
。
只需对方法1的代码做简单的修改:
listViewModel.navigateToDetails.observe(this, Observer {
if (it) {
myViewModel.navigateToDetailsHandled()
startActivity(DetailsActivity...)
}
})
复制代码
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails : LiveData<Boolean>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.value = true
}
fun navigateToDetailsHandled() {
_navigateToDetails.value = false
}
}
复制代码
这种方法的问题是增加了许多冗余的代码,对于每一个事件都需要在ViewModel中添加对应的方法,并且很容易忘记调用ViewModel中的这些方法。
✔️ OK: Use SingleLiveEvent
SingleLiveEvent类就是为解决上述问题而创建的,它是一个只会发送一次数据更新的LiveData。
用法
class ListViewModel : ViewModel {
private val _navigateToDetails = SingleLiveEvent<Any>()
val navigateToDetails : LiveData<Any>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.call()
}
}
复制代码
listViewModel.navigateToDetails.observe(this, Observer {
startActivity(DetailsActivity...)
})
复制代码
SingleLiveEvent的问题在于它仅限于一个观察者。如果您无意中添加了多个观察者,那么只会有一个观察者会对它作出反馈,并且不能保证是哪一个。
✔️ Recommended: Use an Event wrapper
使用这种方法您能明确地知道事件是否已被处理,从而减少错误。
用法
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
复制代码
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails
fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}
复制代码
listViewModell.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})
复制代码
这种方法的好处是用户可以通过调用getContentIfNotHandled()方法来将导航事件与目标观察者关联起来。它把导航事件视为一种状态:consumed 或not consumed。
总而言之:你应当把事件设计为一种状态。你可以自定义一个事件包装器以满足您的需求。
如果你的应用中有许多类似的事件,建议使用EventObserver类来删除一些重复性代码。