本文翻译自【LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)】,详细介绍了 liveData 的使用。感谢做者 Jose Alcérreca。水平有限,欢迎指正讨论。 前面两篇介绍 LiveData 的文章(【译】Android Architecture - ViewModel 与 View 的通讯 和 【译】LiveData 使用详解)都提到了 SingleLiveEvent
,本篇重点来看下它是个什么东西,以及它的使用场景。java
LiveData
通常被用于 View
与 ViewModel
的通讯。View 经过订阅 LiveData 的变化来更新 UI,这适用于须要长时间展现在屏幕上的数据。 android
然而,有些数据可能只须要展现一次,例如 SnackBar
消息,一个 Navigation
事件,或者一个触发 Dialog 展现/消失的数据。 git
咱们不该该尝试用 Architecture Components 基础或扩展库来解决这个问题,相反这是一个设计问题。咱们建议你将这些事件做为数据状态的一部分。在本文中,咱们将展现一些常见错误和推荐方法。github
这种用法是在 LiveData 中保存一个 SnackBar
消息,或一个 Navigation
事件。尽管原则上是 LiveData 的正常使用,但这存在一些问题。 在一个包含首页和详情页的应用中,首页的 ListViewModel.kt 代码以下:c#
// 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 } } 复制代码
MyFragment.kt 代码以下:bash
myViewModel.navigateToDetails.observe(this, Observer {
if (it) startActivity(DetailsActivity...)
})
复制代码
这种使用方式的问题是:_navigateToDetails
中的值会永远为 true
,从而致使没法回到首页。 复现步骤是:app
DetailsActivity
MasterActivity
MasterActivity
由非活动状态恢复到活动状态myViewModel
观察到 _navigateToDetails
仍旧为 true
,就又跳转到详情页 DetailsActivity
一种看起来没问题的解决方案是:页面跳转后立马把标志位设为 false
,如 ListViewModel.kt 所示:mvvm
fun userClicksOnButton() {
_navigateToDetails.value = true
_navigateToDetails.value = false // Don't do this } 复制代码
然而,须要注意的是:LiveData 不能保证发射它接收到的每一个数据值。例如咱们在没有活动的观察者时设置了一个新值,这个新值不会被发送,此外,在多个子线程中操做 LiveData 可能发生竞争情况,从而致使观察者只会收到一次回调。 但这个方案的主要问题是:别人很难看懂这个代码,而且这种代码也很丑陋。那么,咱们应该怎么确保在导航事件发生后恢复初值呢?ide
另外一种稍微好点,但仍有问题的方案是:View 告诉 ViewModel,导航事件已经完成,LiveData 应该恢复默认值了。ui
基于第一节的例子,对观察者代码作以下改动便可,MyFragment.kt:
listViewModel.navigateToDetails.observe(this, Observer {
if (it) {
myViewModel.navigateToDetailsHandled()
startActivity(DetailsActivity...)
}
})
复制代码
而后在 ListViewModel.kt 中添加一个 navigateToDetailsHandled()
方法:
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 中每添加一个事件都要添加一个对应的方法,而且很容易出错。此外,观察者(View)很容易忘记调用 ViewModel 的这个方法。
一种还能够接受的解决方案是:SingleLiveEvent。这个类是 Google 官方 Demo 中的适用于这种特殊场景的解决方案,它是一个仅发送一次更新的 LiveData。
public class SingleLiveEvent<T> extends MutableLiveData<T> {
private static final String TAG = "SingleLiveEvent";
private final AtomicBoolean mPending = new AtomicBoolean(false);
@MainThread
public void observe(LifecycleOwner owner, final Observer<T> 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<T>() {
@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);
}
}
复制代码
ListViewModel.kt 代码以下:
class ListViewModel : ViewModel {
private val _navigateToDetails = SingleLiveEvent<Any>()
val navigateToDetails : LiveData<Any>
get() = _navigateToDetails
fun userClicksOnButton() {
_navigateToDetails.call()
}
}
复制代码
MyFragment.kt 代码以下:
myViewModel.navigateToDetails.observe(this, Observer {
startActivity(DetailsActivity...)
})
复制代码
SingleLiveEvent
的问题在于:它仅限于一个观察者。若是你无心中添加了多个,则只会有一个收到回调,而且没法保证哪个会收到。
推荐的解决方案是:封装事件。经过这种方式,咱们能够明确地管理实践是否被处理,从而减小错误。
Event.kt 封装了事件,代码以下:
/**
* 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 } 复制代码
ListViewModel.kt 代码以下:
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
}
}
复制代码
MyFragment.kt 代码以下:
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. 咱们能够包装本身的 Event 来知足本身的需求。 Bonus! 若是有不少事件,可使用 EventObserver 避免一些样板代码。
我是 xiaobailong24,您能够经过如下平台找到我: