关于android:LiveData的前世今生

这个系列我做了协程和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。
关注我,每天分享常识干货!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理