翻译LiveDataNavigation[译]-LiveData with SnackBar,Navigation and other events(the SingleLiveEvent case)
HumphreyDan原文
view(activity/fragment) 和 ViewModel 交流的比较好的方式是 LiveData observables. view 订阅 LiveData 的改变且随时响应。这适用于连续不断的显示在一个屏幕的数据。
但是某些数据却更应该被消费一次,比如 Snackbar 消息,navigation 事件 或 dialog 触发器。
与其试着通过扩展 Architecture Components 扩展或库解决这个问题,不如我们可以直面这是个设计缺陷。我们推荐你把你的事件看作是状态的一部分。本文我们将列举一些常见的错误和推荐的解决方案。
❌ Bad: 1. 对事件使用 LiveData
在 LiveData 对象内部直接持有 Snackbar 消息或 navigation 信号。原则上普通的 LiveData 对象可以这样使用,但实际上会暴露一些问题。
在 master/detail 架构的 app 中,如下是 maters 的 ViewModel
1 2 3 4 5 6 7 8 9 10
| class ListViewModel : ViewModel { private val _navigateToDetails = MutableLiveData<Boolean>() val navigateToDetails : LiveData<Boolean> get() = _navigateToDetails
fun userClicksOnButton() { _navigateToDetails.value = true } }
|
在 View(activity/fragment) 中
1 2 3
| myViewModel.navigateToDetails.observe(this,Observer { if (it) startActivity(DetailsActivity....) })
|
此方案的不足在于 _navigateToDetails
将会一直为 true,而且不可能回到首屏:
- 用户点击按钮启动 Details Activity
- 用户按返回按钮,回到主 Activity
- 当 activity 进入回退栈时 observers 失活,现在再次激活
从 ViewModel 中调用 navigation 且立即将其设为 false
1 2 3 4
| fun userClicksOnButton() { _navigateToDetails.value = true _navigateToDetails.value = false }
|
但是请注意: LiveData 保存数据但不会保证在接受到事件时发送任何数据。例如,当没有观察者活跃时更新值,那么一个新值将替换原来的值。同时,从不同线程设置属性将会导致竞争状态,此时仅能保证一个观察者被调用。
最主要的问题是,这个方案很难理解而且代码垃圾。所以我们如何保证在 navigation 事件发生时值重置?
❌Better: 2.使用 LiveData wrapper 事件,在观察者中重置属性.
1 2 3 4 5 6
| listViewModel.navigateToDetails.observe(this,Observer { if (it) { myViewModel.navigateToDetailsHandled() startActivity(DetailsActivity...) } })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class ListViewModel: ViewModel { private val _navigateToDetails = MutableLiveData<Boolean>()
val navigateToDetails: LiveData<Boolean> get() = _navigateToDetails
fun userClicksOnButton() { _navigateToDetails.value = true }
fun navigateToDetailsHandled() { _navigateToDetails.value = false } }
|
此方案的不足之处在于有些冗余代码
✅ ok:使用 SingleLiveEvent
SingleLiveEvent 只适用于部分场景。只发送和更新一次状态的 LiveData
1 2 3 4 5 6 7 8 9 10
| 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...) })
|
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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."); }
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); }
@MainThread public void call() { setValue(null); } }
|
此方案的不足之处在于只有一个订阅者。如果你有多个观察者,那么只有一个被调用且不保证顺序。
✅ 推荐:使用 Event Wrapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| open class Event<out T>(private val content: T) { val hasBeenHandled = false private set
|
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) } }
|
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) } }
|
此方案的优势是用户需要使用 getContentIfNotHandled() 或 peekContent()
指定意图。此方法把事件抽象为 state 的一部分:变成仅表示是否被消费的消息。
结论
总之,把事件作为状态的一部分。
使用这个 EventObserver 在大量事件结束后移除它
1 2 3 4 5 6 7
| class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> { override fun onChanged(event: Event<T>?) { event?.getContentIfNotHandled()?.let { value -> onEventUnhandledContent(value) } } }
|
1 2 3
| inline fun <T> LiveData<Event<T>>.observeEvent(owner: LifecycleOwner, crossinline onEventUnhandledContent: (T) -> Unit) { observe(owner, Observer { it?.getContentIfNotHandled()?.let(onEventUnhandledContent) }) }
|