这个系列我做了协程和 Flow 开发者的一系列文章的翻译,旨在理解以后协程、Flow、LiveData 这样设计的起因,从设计者的角度,发现他们的问题,以及如何解决这些问题,pls enjoy it。
这篇文章是剖析 LiveData 重放净化最早的一篇文章,同时作者也给出了根本的解决方案,这也是后续 Flow 的应用场景之一。
LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)
View(Activity 或 Fragment)与 ViewModel 通信的一个便捷形式就是应用 LiveData 来察看变量。View 订阅 LiveData 中的变动,并对其做出反馈。这对于在屏幕上间断显示并可能会批改的数据来说是十分无效的伎俩。
<figcaption style=”margin: 5px 0px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;”>img</figcaption>
然而,有些数据应该只被耗费一次,比如说 Snackbar 音讯、导航事件或对话框相似的场景。
与其试图用库或架构组件来解决这个问题,不如把它作为一个设计问题来面对。咱们倡议你把你的事件作为 View 状态的一部分。在这篇文章中,咱们展现了一些常见的谬误和举荐的办法。
Bad: 1. Using LiveData for events
这种办法是在 LiveData 对象中间接保留一个 Snackbar 音讯或导航的标记量。尽管从原则上看,一般的 LiveData 对象的确能够用于此,但它也带来了一些问题。
在一个 List/Detail 模式中,这里是列表的 ViewModel。
// 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):
myViewModel.navigateToDetails.observe(this, Observer {if (it) startActivity(DetailsActivity...)
})
这种办法的问题是,_navigateToDetails 中的值在很长一段时间内都是 True,所以它不可能回到第一个界面。咱们一步一步来看。
- 用户点击按钮,于是跳转了 Detail 界面
- 用户按下返回键,回到列表界面中去
- 观察者在 Activity 处于 Pause 的堆栈中时,会变成不活动状态,返回时,会再次成为活动状态
- 但此时,察看的值依然是 True,所以 Detail 界面被谬误地再次启动
一个解决方案是,从 ViewModel 启动导航后,立刻将标记设置为 false。
fun userClicksOnButton() {
_navigateToDetails.value = true
_navigateToDetails.value = false // Don't do this
}
然而,你须要记住的一件事是,LiveData 持有数值,但并不保障发射它所收到的每一个数值。例如:一个值能够在没有观察者流动的状况下被设置,所以新的观察者会间接取代它。另外,从不同的线程设置值可能会导致比赛条件,只产生一个对观察者的调用。
但后面这种解决办法的次要问题是,它很难了解,而且很难看,同时,咱们如何确保在导航事件产生后值能被正确的重置?
Better: 2. Using LiveData for events, resetting event values in observer
通过这种办法,你增加了一种办法,从视图中表明你曾经解决了该事件,并且它应该被重置。
应用办法如下。
只有对咱们的观察者做一个小小的扭转,咱们就能够解决这个问题了。
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。
OK: Use 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...)
})` </pre>
SingleLiveEvent 的示例代码如下所示。
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.architecture.blueprints.todoapp;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Observer;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
* <p>
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
* <p>
* Note that only one observer is going to be notified of changes.
*/
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);
}
}
然而,SingleLiveEvent 的问题是,它被限度在一个观察者身上。如果你不小心减少了一个以上的观察者,只有一个会被调用,而且不能保障是哪一个。
Recommended: Use an Event wrapper
在这种解决办法中,你能够明确地治理事件是否被解决,从而缩小谬误。应用办法如下所示。
/**
* 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() 来指定用意。这种办法将事件建模为状态的一部分:它们当初只是一个曾经被生产或未被生产的音讯。
综上所述:将事件设计成你的状态的一部分。在 LiveData 观测器中应用你本人的 EventWrapper,并依据你的须要对其进行定制。
另外,如果你有大量的事件,能够应用这个 EventObserver 来防止一些反复的模板代码。
https://gist.github.com/JoseA…
LiveData with single events
你能够在互联网上搜寻 SingleLiveEvent,它为一次性事件的 LiveData 找到一个好的解决方案。
The problem
问题开始了,因为 LiveData 文档中解释了一些劣势,你能够在其文档中找到这些劣势,我顺便在这里列出了这些长处。
- 确保你的用户界面与你的数据状态相匹配:LiveData 遵循观察者模式,当生命周期状态扭转时,LiveData 会告诉观察者对象。你能够整合你的代码来更新这些观察者对象中的 UI。你的观察者能够在每次利用数据变动(生命周期变动)时更新 UI,而不是在每次有变动时更新 UI。
- 没有内存透露:观察者被绑定到生命周期对象,并在其相干的生命周期被销毁时进行自我清理。
-
不会因为 Activity 的销毁而解体:如果观察者的生命周期处于非活动状态,例如在后堆栈中的流动,那么它就不会收到任何 LiveData 事件。
- 不再须要手动解决生命周期:UI 组件只是察看相干的数据,而不须要被动进行或复原察看。LiveData 会主动治理这所有,因为它在察看时就晓得相干的生命周期状态变动。
- 始终保持最新的数据:如果一个组件的生命周期变得不沉闷,那它在再次变得沉闷时就会收到最新的数据。例如,一个处于后盾的 Activity 在回到前台后会立刻收到最新的数据。
- 配置变动时更新:如果一个 Activity 或 Fragment 因为配置变动而被从新创立,比方设施旋转,它就会立刻接管最新的可用数据。
- 共享资源:你能够应用单例模式扩大一个 LiveData 对象,以包装零碎服务,这样它们就能够在你的应用程序中被共享。LiveData 对象与零碎服务连贯一次,而后任何须要该资源的观察者就能够察看 LiveData 对象。欲了解更多信息,请参见扩大 LiveData。
https://developer.android.com…
然而,这些劣势中的一些场景,并不会在所有状况下都发挥作用,而且在实例化 LiveData 的时候也没有方法禁用它们。例如,” 始终保持最新数据 ” 这个个性就不能被禁用,而本文想要解决的次要问题就是如何禁用它。
然而,我必须感激谷歌提供的 “ 适当的配置变更 “ 属性,它是如此的有用。但咱们依然须要可能在咱们想要的时候禁用它。我没有须要禁用它的场景,但能够让人们抉择。
The suggested ways to solve the problem
读完 Jose 的文章后,你能够在这里找到他举荐的解决方案的主类的 github 源代码。
/**
* 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}
然而有一个叫 feinstein 的人在该页面中提出了两个无效的问题。
- Jose 的解决方案不足对多个观察者的反对,而这正是 LiveData 以 “ 共享资源 “ 为名的承诺之一。
- 它不是线程平安的。
我还能够补充一个问题。通过应用 LiveData,咱们心愿在代码中应用函数式编程的劣势,而函数式编程的准则之一是应用不可变的数据结构。这个准则将被 Jose 举荐的解决方案所突破。
在 Jose 之后,Kenji 试图解决 “ 共享资源 “ 的问题。
class SingleLiveEvent2<T> : MutableLiveData<T>() {private val pending = AtomicBoolean(false)
private val observers = mutableSetOf<Observer<T>>()
private val internalObserver = Observer<T> { t ->
if (pending.compareAndSet(true, false)) {
observers.forEach { observer ->
observer.onChanged(t)
}
}
}
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T>) {observers.add(observer)
if (!hasObservers()) {super.observe(owner, internalObserver)
}
}
override fun removeObservers(owner: LifecycleOwner) {observers.clear()
super.removeObservers(owner)
}
override fun removeObserver(observer: Observer<T>) {observers.remove(observer)
super.removeObserver(observer)
}
@MainThread
override fun setValue(t: T?) {pending.set(true)
super.setValue(t)
}
@MainThread
fun call() {value = null}
}
然而正如你所看到的,internalObserver 被传递给 super.observe 办法一次,所以它对第一个所有者察看了一次,其余的所有者都被抛弃了,谬误的行为从这里开始。这个类的另一个不好的行为是,removeObserver 没有像预期的那样工作,因为在 removeObserver 办法中,internalObserver 的实例会被找回来,它不在汇合中。所以没有任何货色会被从汇合中移除。
The recommended solution
你能够在 LiveData 类自身中找到解决多个观察者的规范办法,那就是将原始观察者包裹起来。因为 LiveData 类不容许咱们拜访它的 ObserverWrapper 类,咱们必须创立咱们的版本。
ATTENTION: PLEASE LOOK AT THE SECOND UPDATE SECTION
class SingleLiveEvent<T> : MutableLiveData<T>() {private val observers = CopyOnWriteArraySet<ObserverWrapper<T>>()
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T>) {val wrapper = ObserverWrapper(observer)
observers.add(wrapper)
super.observe(owner, wrapper)
}
override fun removeObservers(owner: LifecycleOwner) {observers.clear()
super.removeObservers(owner)
}
override fun removeObserver(observer: Observer<T>) {observers.remove(observer)
super.removeObserver(observer)
}
@MainThread
override fun setValue(t: T?) {observers.forEach { it.newValue() }
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {value = null}
private class ObserverWrapper<T>(private val observer: Observer<T>) : Observer<T> {private val pending = AtomicBoolean(false)
override fun onChanged(t: T?) {if (pending.compareAndSet(true, false)) {observer.onChanged(t)
}
}
fun newValue() {pending.set(true)
}
}
}
首先,这个类是线程平安的,因为观察者属性是 final 的,CopyOnWriteArraySet 也是线程平安的。其次,每个观察者都会以本人的所有者身份注册到父级 LiveData。第三,在 removeObserver 办法中,咱们心愿有一个 ObserverWrapper,咱们曾经在 observe 办法中注册了这个 ObserverWrapper,并且咱们在 observices 中设置了它来移除。所有这些都意味着咱们正确地反对 “ 共享资源 “ 属性。
11/2018 更新
正如我团队中的一位成员所提到的,我遗记了在 removeObservers 办法中解决所有者:LifecycleOwner!这可能是一个问题。如果在你的应用程序的一个页面中,你有多个 Fragments 作为 LifecycleOwner 和一个 ViewModel,这可能是一个问题。让我纠正一下我的解决方案。
class LiveEvent<T> : MediatorLiveData<T>() {private val observers = ConcurrentHashMap<LifecycleOwner, MutableSet<ObserverWrapper<T>>>()
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T>) {val wrapper = ObserverWrapper(observer)
val set = observers[owner]
set?.apply {add(wrapper)
} ?: run {val newSet = Collections.newSetFromMap(ConcurrentHashMap<ObserverWrapper<T>, Boolean>())
newSet.add(wrapper)
observers[owner] = newSet
}
super.observe(owner, wrapper)
}
override fun removeObservers(owner: LifecycleOwner) {observers.remove(owner)
super.removeObservers(owner)
}
override fun removeObserver(observer: Observer<T>) {
observers.forEach {if (it.value.remove(observer)) {if (it.value.isEmpty()) {observers.remove(it.key)
}
return@forEach
}
}
super.removeObserver(observer)
}
@MainThread
override fun setValue(t: T?) {observers.forEach { it.value.forEach { wrapper -> wrapper.newValue() } }
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {value = null}
private class ObserverWrapper<T>(private val observer: Observer<T>) : Observer<T> {private val pending = AtomicBoolean(false)
override fun onChanged(t: T?) {if (pending.compareAndSet(true, false)) {observer.onChanged(t)
}
}
fun newValue() {pending.set(true)
}
}
}
除了后面的参数之外,这也是线程平安的,因为 ConcurrentHashMap 是线程平安的。在这里,咱们应该增加一个提醒。你能够在你的代码中定义以下扩大。
fun <T> LiveData<T>.toSingleEvent(): LiveData<T> {val result = LiveEvent<T>()
result.addSource(this) {result.value = it}
return result
}
而后,如果想有一个繁多的事件,只需在你的 ViewModel 中像这样调用这个扩大办法。
class LiveEventViewModel {
...
private val liveData = MutableLiveData<String>()
val singleLiveEvent = liveData.toSingleEvent()
...
... {liveData.value = "YES"}
}
而且你能够像其余 LiveDatas 一样应用这个 singleLiveEvent。
关注我,每天分享常识干货!