[译] 在 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 链接。


本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏web前端教室

Redux基本用法,为周日的先行者课程准备着。

众所周知,React只是一个View层而已,它不是一个完整的前端解决方案。只是给出了页面组件化的解决思路,但组件之间如何沟通?代码之间的结构?它并没有给出更多的...

1917
来自专栏增长技术

Android 应用架构演变

代码被划分为两层结构:Data Layer(数据层)负责从数据源获取和存储数据;View Layer(视图层)负责处理并将数据展示在UI上

862
来自专栏IMWeb前端团队

React入门心得及使用tips

1 前言 React目前在前端的影响力就不多说了,不管你目前有没有入坑React,估计都见过不少各种相关的新闻和技术文章。如果你有入坑React的打算,或者刚开...

1785
来自专栏图像识别与深度学习

《Android》Lesson12-自定义布局

2218
来自专栏编程

组件化通用模式

一、前言 模式是一种规律或者说有效的方法,所以掌握某一种实践总结出来的模式是快速学习和积累的较好方法,模式的对错需要自己去把握,但是只有量的积累才会发生质的改变...

1677
来自专栏我就是马云飞

Architecture Components 生命周期

前言 最近这几天一直在研究官方的MVVM的实现,使用起来其实难度并不大,并且如果结合DataBinding和Dagger2 代码写的都要飞起来了,不要太好。不过...

1885
来自专栏编程

前端三大框架大杂烩

摘要:从angular的诞生独步天下,到现在三大框架平分天下,基本形势已经趋于稳定。每一个框架从诞生到受欢迎,都有其特定的原因和背景。不同的开发者选择时,也是依...

2525
来自专栏卡少编程之旅

REACT框架学习心得

3127
来自专栏企鹅号快讯

前端三大框架vue,angular,react大杂烩

摘要:从angular的诞生独步天下,到现在三大框架平分天下,基本形势已经趋于稳定。每一个框架从诞生到受欢迎,都有其特定的原因和背景。不同的开发者选择时,也是依...

2219
来自专栏Keegan小钢

App项目实战之路(三):原型篇

本来,我是没打算写原型篇的,但考虑到关注我的人中也有部分产品狗,更重要的是,我一直认为,不懂产品设计的程序猿不是优秀的产品经理。而且,应该也有不少程序猿想往产品...

1113

扫码关注云+社区