(译) 将 LiveData 用于 SnackBar, Navigation 以及其它事件(单次事件的情况)

原文链接

对于视图(Activity 或 Fragment)与 ViewModel 通信来说,使用 LiveData 可观察对象是一种便捷的方式。该视图订阅 LiveData 中的更改并对其作出响应。这适用于连续显示在屏幕上的数据。

但是,某些数据应该只消耗一次,如Snackbar消息,导航事件或对话框触发器。

不要试图用架构组件的库或扩展来解决这个问题,而是应该把它看成一个设计问题。我们建议你将你的事件当成你状态的一部分。在本文中,我们将展示一些常见错误和建议的方法。

❌ 不好的:将 LiveData 用于事件

此方法直接在 LiveData 对象中包含 Snackbar 消息或导航信号。虽然原则上似乎可以使用常规的LiveData对象,但它存在一些问题。

在主/详细应用中,这是主的 ViewModel:

1
2
3
4
5
6
7
8
9
10
11
12
// Don't 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 )

1
2
3
myViewModel.navigateToDetails.observe(this, Observer {
if (it) startActivity(DetailsActivity...)
})

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

  1. 用户单击该按钮以启动详情 Activity
  2. 用户按下,返回主 Activity
  3. 在 Activity 处于后台堆栈时,观察者在处于非活动状态后再次变为活动状态
  4. 该值仍然为true,因此详情页再次错误地启动
    解决方案是从ViewModel触发导航并立即将标志设置为false:
1
2
3
4
fun userClicksOnButton() {
_navigateToDetails.value = true
_navigateToDetails.value = false // Don't do this
}

但是,要记住的一件重要事情是 LiveData 保存值但不保证发出它接收的每个值。例如:当没有观察者处于活动状态时可以设置一个值,因此新的将只替换它。此外,从不同线程设置值可能会导致竞争条件,这只会产生一次对观察者的调用。

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

❌ 好一点:将 LiveData 用于事件,在观察者中重置事件值

使用此方法,您可以添加一种方法来从View中指示您已经处理了事件并且应该重置它。

用法

通过对我们的观察者进行一些小改动,我们可能会有一个解决方案:

1
2
3
4
5
6
listViewModel.navigateToDetails.observe(this, Observer {
if (it) {
myViewModel.navigateToDetailsHandled()
startActivity(DetailsActivity...)
}
})

在 ViewModel 中添加新方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。

用法

1
2
3
4
5
6
7
8
9
10
11
class ListViewModel : ViewModel {
private val _navigateToDetails = SingleLiveEvent<Any>()

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


fun userClicksOnButton() {
_navigateToDetails.call()
}
}
1
2
3
myViewModel.navigateToDetails.observe(this, Observer {
startActivity(DetailsActivity...)
})

问题

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

✔️ 推荐的:使用 Event wrapper

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

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 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
}
1
2
3
4
5
6
7
8
9
10
11
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
}
}
1
2
3
4
5
myViewModel.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})

这种方法的优点是用户需要通过使用 getContentIfNotHandled() 或 peekContent()来指定意图。此方法将事件建模为状态的一部分:它们现在只是已消耗或未消耗的消息。

总结:将事件设计成你的状态的一部分。在 LiveData observable 中 使用你自己的 Event包装器并根据你的需要对其进行自定义。

奖金!如果最终有很多事件,请使用此 EventObserver 删除一些重复的代码。

非典型前端coder wechat
想要随时Follow我的最新博客,可扫码关注我的公众号