带你了解LiveData重放污染的前世今生

Android群英传

共 26781字,需浏览 54分钟

 · 2021-12-01

点击上方蓝字关注我,知识会给你力量


这个系列我做了协程和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中的变化,并对其做出反应。这对于在屏幕上连续显示并可能会修改的数据来说是非常有效的手段。

img

然而,有些数据应该只被消耗一次,比如说Snackbar消息、导航事件或对话框类似的场景。

img

与其试图用库或架构组件来解决这个问题,不如把它作为一个设计问题来面对。我们建议你把你的事件作为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...)
})

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的问题是,它被限制在一个观察者身上。如果你不小心增加了一个以上的观察者,只有一个会被调用,而且不能保证是哪一个。

img

✔️ 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()来指定意图。这种方法将事件建模为状态的一部分:它们现在只是一个已经被消费或未被消费的消息。

img

综上所述:将事件设计成你的状态的一部分。在LiveData观测器中使用你自己的EventWrapper,并根据你的需要对其进行定制。

另外,如果你有大量的事件,可以使用这个EventObserver来避免一些重复的模板代码。

https://gist.github.com/JoseAlcerreca/e0bba240d9b3cffa258777f12e5c0ae9

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/topic/libraries/architecture/livedata#extend_livedata

但是,这些优势中的一些场景,并不会在所有情况下都发挥作用,而且在实例化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(truefalse)) {
            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(truefalse)) {
                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(truefalse)) {
                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。

02/2019年更新

正如杰弗里-麦克纳利-道斯在回应部分正确指出的那样,我之前的解决方案中存在一个错误! 我注意到,我的一个假设是错误的,所以我达到了错误的解决方案! 为了永远解决这个问题,我创建了一个库,里面有足够的测试来显示我的假设并验证它们。你可以在https://github.com/hadilq/LiveEvent 找到这个库,在这里找到LiveEvent类。如果有任何其他问题,你可以直接提出拉动请求,说明我错了,错在哪里。另外,你可以通过Maven导入库,而不是复制/粘贴LiveEvent类,这样,一旦库的版本更新,任何错误修复都会出现在你的项目中。

2020年1月更新

这个问题还有一个解决方案,你可以用PublishProcessor代替LiveEvent,用我的新文章https://medium.com/@hadilq.dev/rxjava-instead-of-livedata-in-mvvm-f95e8fe0aa41 来观察它。

最后,我非常乐意看到你对它的反馈。

原文链接:https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

https://proandroiddev.com/livedata-with-single-events-2395dea972a8

向大家推荐下我的网站 https://xuyisheng.top/  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问



往期推荐


本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。
< END >
作者:徐宜生

更文不易,点个“三连”支持一下👇


浏览 19
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报