阅读 243

[译]带有SnackBar、Navigation和其他事件的LiveData

视图(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,导致无法返回到列表界面。详细情况如下:

  1. 用户在列表界面中点击按钮进入详情界面。
  2. 用户按下返回键返回到列表界面。
  3. 列表界面的观察者观察到_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类来删除一些重复性代码。

关注下面的标签,发现更多相似文章
评论