博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【译】LiveData 在 SnackBar/Navigation 情景下的使用(SingleLiveEvent)
阅读量:6198 次
发布时间:2019-06-21

本文共 5461 字,大约阅读时间需要 18 分钟。

前言

本文翻译自【】,详细介绍了 liveData 的使用。感谢作者 。水平有限,欢迎指正讨论。 前面两篇介绍 LiveData 的文章( 和 )都提到了 SingleLiveEvent,本篇重点来看下它是个什么东西,以及它的使用场景。

正文

LiveData 一般被用于 ViewViewModel 的通信。View 通过订阅 LiveData 的变化来更新 UI,这适用于需要长时间展示在屏幕上的数据。

然而,有些数据可能只需要展示一次,例如 SnackBar 消息,一个 Navigation 事件,或者一个触发 Dialog 展示/消失的数据。

我们不应该尝试用 基础或扩展库来解决这个问题,相反这是一个设计问题。我们建议你将这些事件作为数据状态的一部分。在本文中,我们将展示一些常见错误和推荐方法。

❌ Bad: 1. Using LiveData for events

这种用法是在 LiveData 中保存一个 SnackBar 消息,或一个 Navigation 事件。尽管原则上是 LiveData 的正常使用,但这存在一些问题。 在一个包含首页和详情页的应用中,首页的 代码如下:

// Don't use this for eventsclass ListViewModel : ViewModel {    private val _navigateToDetails = MutableLiveData
() val navigateToDetails : LiveData
get() = _navigateToDetails fun userClicksOnButton() { _navigateToDetails.value = true }}复制代码

代码如下:

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

这种使用方式的问题是:_navigateToDetails 中的值会永远为 true,从而导致无法回到首页。 复现步骤是:

  1. 用户点击按钮,启动详情页 DetailsActivity
  2. 用户点击返回键,返回到主界面 MasterActivity
  3. 这时 MasterActivity 由非活动状态恢复到活动状态
  4. myViewModel 观察到 _navigateToDetails 仍旧为 true,就又跳转到详情页 DetailsActivity

一种看起来没问题的解决方案是:页面跳转后立马把标志位设为 false,如 所示:

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

另一种稍微好点,但仍有问题的方案是:View 告诉 ViewModel,导航事件已经完成,LiveData 应该恢复默认值了。

Usage

基于第一节的例子,对观察者代码做如下改动即可,:

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

然后在 中添加一个 navigateToDetailsHandled() 方法:

class ListViewModel : ViewModel {    private val _navigateToDetails = MutableLiveData
() val navigateToDetails : LiveData
get() = _navigateToDetails fun userClicksOnButton() { _navigateToDetails.value = true } fun navigateToDetailsHandled() { _navigateToDetails.value = false }}复制代码

Issues

这种方法的问题是:存在很多样板代码,ViewModel 中每添加一个事件都要添加一个对应的方法,并且很容易出错。此外,观察者(View)很容易忘记调用 ViewModel 的这个方法。

✅ OK: Use SingleLiveEvent

一种还可以接受的解决方案是:。这个类是 Google 官方 Demo 中的适用于这种特殊场景的解决方案,它是一个仅发送一次更新的 LiveData。

public class SingleLiveEvent
extends MutableLiveData
{ private static final String TAG = "SingleLiveEvent"; private final AtomicBoolean mPending = new AtomicBoolean(false); @MainThread public void observe(LifecycleOwner owner, final Observer
observer) { if (hasActiveObservers()) { Log.w(TAG, "Multiple observers registered but only one will be notified of changes."); } // Observe the internal MutableLiveData super.observe(owner, new Observer
() { @Override public void onChanged(@Nullable T t) { if (mPending.compareAndSet(true, false)) { observer.onChanged(t); } } }); } @MainThread public void setValue(@Nullable T t) { mPending.set(true); super.setValue(t); } /** * Used for cases where T is Void, to make calls cleaner. */ @MainThread public void call() { setValue(null); }}复制代码

Usage

代码如下:

class ListViewModel : ViewModel {    private val _navigateToDetails = SingleLiveEvent
() val navigateToDetails : LiveData
get() = _navigateToDetails fun userClicksOnButton() { _navigateToDetails.call() }}复制代码

代码如下:

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

Issues

SingleLiveEvent 的问题在于:它仅限于一个观察者。如果你无意中添加了多个,则只会有一个收到回调,并且无法保证哪一个会收到。

✅ Recommended: Use an Event wrapper

推荐的解决方案是:封装事件。通过这种方式,我们可以明确地管理实践是否被处理,从而减少错误。

Usage

封装了事件,代码如下:

/** * Used as a wrapper for data that is exposed via a LiveData that represents an event. */open class Event
(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
>() val navigateToDetails : LiveData
> 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 {    // Only proceed if the event has never been handled    it.getContentIfNotHandled()?.let {        startActivity(DetailsActivity...)    }})复制代码

这种方案的优势在于:用户需要调用 Event#getContentIfNotHandled() 方法或 Event#peekContent() 来指定跳转 Intent。这种方案将事件作为 UI 状态的一部分:现在它们只是一个已被消费或未被消费的消息。

总结

design events as part of your state. 我们可以包装自己的 来满足自己的需求。 Bonus! 如果有很多事件,可以使用 避免一些样板代码。

参考

联系

我是 xiaobailong24,您可以通过以下平台找到我:

  • Github:
  • 简书:
  • 掘金:

转载地址:http://udnca.baihongyu.com/

你可能感兴趣的文章
IPv6 to IPv4自动隧道实验笔记
查看>>
jCarousel jQuery下的滚动切换传送插件
查看>>
Saltstack --crontab定时任务管理
查看>>
使用AutoMySQLBackup 自动备份MySQL数据库
查看>>
Zabbix3.4分布式监控----zabbix_proxy
查看>>
华为AC双机热备(双链路热备)
查看>>
seo
查看>>
如何删除“无法删除文件,无法读取源文件或磁盘”文件
查看>>
几个简单的html+css+js题目
查看>>
webpack config
查看>>
codeforces 961D Pair Of Lines
查看>>
Oracle 18c 数据库中scott用户不存在的解决方法
查看>>
1.计算机发展阶段 计算机发展历史 机械式计算机 机电式计算机 电子计算机 逻辑电路与计算机 二极管 电子管 晶体管 硅 门电路 计算机 电磁学计算机二进制...
查看>>
《算法导论》读书笔记之第15章 动态规划[总结]
查看>>
js实现图片轮播(终结版)
查看>>
Exchange 常见问题之二
查看>>
殊不知,互联网运营才能实现最高效营销!
查看>>
linux常用命令之查阅文件
查看>>
两种方式建立Vsftpd虚拟用户
查看>>
switch的用法
查看>>