[译] 在 SnackBar,Navigation 和其他事件中使用 LiveData(SingleLiveEvent 案例)

视图层(Activity 或者 Fragment)与 ViewModel 层进行通讯的一种便捷的方式就是使用 LiveData 来进行观察。这个视图层订阅 Livedata 的数据变化并对其变化做出反应。这适用于连续不断显示在屏幕的数据。

但是,有一些数据只会消费一次,就像是 Snackbar 消息,导航事件或者对话框。

这应该被视为设计问题,而不是试图通过架构组件的库或者扩展来解决这个问题。我们建议您将您的事件视为您的状态的一部分。在本文中,我们将展示一些常见的错误方法,以及推荐的方式。

❌ 错误:1. 使用 LiveData 来解决事件

这种方法来直接的在 LiveData 对象的内部持有 Snackbar 消息或者导航信息。尽管原则上看起来像是普通的 LiveData 对象可以用在这里,但是会出现一些问题。

在一个主/从应用程序中,这里是主 ViewModel:

// 不要使用这个事件
class ListViewModel : ViewModel {
    private val _navigateToDetails = MutableLiveData<Boolean>()

    val navigateToDetails : LiveData<Boolean>
        get() = _navigateToDetails


    fun userClicksOnButton() {
        _navigateToDetails.value = true
    }
}
复制代码

在视图层(Activity 或者 Fragment):

myViewModel.navigateToDetails.observe(this, Observer {
    if (it) startActivity(DetailsActivity...)
})
复制代码

这种方法的问题是 _navigateToDetails 中的值会长时间保持为真,并且无法返回到第一个屏幕。一步一步进行分析:

  1. 用户点击按钮 Details Activity 启动。
  2. 用户用户按下返回,回到主 Activity。
  3. 观察者在 Activity 处于回退栈时从非监听状态再次变成监听状态。
  4. 但是该值仍然为 “真”,因此 Detail Activity 启动出错。

解决方法是从 ViewModel 中将导航的标志点击后立刻设为 false;

fun userClicksOnButton() {
    _navigateToDetails.value = true
    _navigateToDetails.value = false // Don't do this
}
复制代码

但是,需要记住的一件很重要的事就是 LiveData 储存这个值,但是不保证发出它接受到的每个值。例如:当没有观察者处于监听状态时,可以设置一个值,因此新的值将会替换它。此外,从不同线程设置值的时候可能会导致资源竞争,只会向观察者发出一次改变信号。

但是这种方法的主要问题是难以理解和不简洁。在导航事件发生后,我们如何确保值被重置呢?

❌ 可能更好一些:2. 使用 LiveData 进行事件处理,在观察者中重置事件的初始值

通过这种方法,您可以添加一种方法来从视图中支出您已经处理了该事件,并且重置该事件。

用法

对我们的观察者进行一些小改动,我们就有了这样的解决方案:

listViewModel.navigateToDetails.observe(this, Observer {
    if (it) {
        myViewModel.navigateToDetailsHandled()
        startActivity(DetailsActivity...)
    }
})
复制代码

像下面这样在 ViewModel 中添加新的方法:

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 的方法。

✔️ 正确解决方法: 使用 SingleLiveEvent

这个 SingleLiveEvent 类是为了适用于特定场景的解决方法。这是一个只会发送一次更新的 LiveData。

用法
class ListViewModel : ViewModel {
    private val _navigateToDetails = SingleLiveEvent<Any>()

    val navigateToDetails : LiveData<Any>
        get() = _navigateToDetails


    fun userClicksOnButton() {
        _navigateToDetails.call()
    }
}
复制代码
myViewModel.navigateToDetails.observe(this, Observer {
    startActivity(DetailsActivity...)
})
复制代码
问题

SingleLiveEvent 的问题在于它仅限于一个观察者。如果您无意中添加了多个,则只会调用一个,并且不能保证哪一个。

✔️ 推荐: 使用事件包装器

在这种方法中,您可以明确地管理事件是否已经被处理,从而减少错误。

用法
/**
 * 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
    }
}
复制代码
myViewModel.navigateToDetails.observe(this, Observer {
    it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
        startActivity(DetailsActivity...)
    }
})
复制代码

这种方法的优点在于用户使用 getContentIfNotHandled() 或者 peekContent() 来指定意图。这个方法将事件建模为状态的一部分:他们现在只是一个消耗或者不消耗的消息。

使用事件包装器,您可以将多个观察者添加到一次性事件中。


总之:把事件设计成你的状态的一部分。使用您自己的事件包装器并根据您的需求进行定制。

银弹!若您最终发生大量事件,请使用这个 EventObserver 可以删除很多无用的代码。

感谢 Don TurnerNick Butcher,和 Chris Banes

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值