The past and present life of LiveData

Keywords: Android Back-end

In this series, I translated a series of articles written by the developers of collaborative process and Flow, in order to understand the reasons for the current design of collaborative process, Flow and LiveData, find their problems from the perspective of designers and how to solve them. pls enjoy it.

This article is the first to analyze the replay pollution of LiveData. At the same time, the author also gives the basic solution, which is also one of the subsequent use scenarios of Flow.

LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)

A convenient way for view (Activity or Fragment) to communicate with ViewModel is to use LiveData to observe variables. View subscribes to and responds to changes in LiveData. This is a very effective means for continuously displaying and possibly modifying data on the screen.

<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>

However, some data should be consumed only once, such as Snackbar messages, navigation events, or dialog boxes.

Instead of trying to solve this problem with libraries or architectural components, it's better to face it as a design problem. We recommend that you make your event part of the View state. In this article, we show some common mistakes and recommended methods.

Bad: 1. Using LiveData for events

This method is to directly save a Snackbar message or navigation flag in the LiveData object. Although in principle, ordinary LiveData objects can be used for this, it also brings some problems.

In a List/Detail mode, here is the ViewModel of the list.

// 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
    }
}

In view (Activity or Fragment):

myViewModel.navigateToDetails.observe(this, Observer {
    if (it) startActivity(DetailsActivity...)
})

The problem with this approach is_ The value in navigateToDetails is True for a long time, so it is impossible to return to the first interface. Let's look at it step by step.

  • The user clicks the button and jumps to the Detail interface
  • The user presses the back key to return to the list interface
  • When the observer is in the stack of Pause, it will become inactive. When it returns, it will become active again
  • However, at this time, the observed value is still True, so the Detail interface is started again by mistake

One solution is to set the flag to false immediately after starting navigation from the ViewModel.

fun userClicksOnButton() {
    _navigateToDetails.value = true
    _navigateToDetails.value = false // Don't do this
}

However, one thing you need to remember is that LiveData holds values, but it is not guaranteed to transmit every value it receives. For example, a value can be set without observer activity, so a new observer will directly replace it. In addition, setting values from different threads may lead to race conditions, resulting in only one call to the observer.

But the main problem of the previous solution is that it is difficult to understand and ugly. At the same time, how can we ensure that the value can be reset correctly after the navigation event?

Better: 2. Using LiveData for events, resetting event values in observer

In this way, you add a method to indicate from the view that you have handled the event and that it should be reset.

The use method is as follows.

As long as we make a small change to our observers, we can solve this problem.

listViewModel.navigateToDetails.observe(this, Observer {
    if (it) {
        myViewModel.navigateToDetailsHandled()
        startActivity(DetailsActivity...)
    }
})

Add a new method to the ViewModel, as shown below.

class ListViewModel : ViewModel {
    private val _navigateToDetails = MutableLiveData<Boolean>()

    val navigateToDetails : LiveData<Boolean>
        get() = _navigateToDetails

    fun userClicksOnButton() {
        _navigateToDetails.value = true
    }

    fun navigateToDetailsHandled() {
        _navigateToDetails.value = false
    }
}

The problem with this solution is that there are some template code in the code (each event has one or more new methods in ViewModel), and it is error prone; It's easy to forget to call ViewModel from the observer.

OK: Use SingleLiveEvent

The SingleLiveEvent class is created for a sample as a valid and recommended solution for the specific scenario. It is a LiveData, but only sends an update once.

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>

The sample code for SingleLiveEvent is shown below.

/*
 *  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);
    }
}

However, the problem with SingleLiveEvent is that it is limited to one observer. If you accidentally add more than one observer, only one will be called, and you can't guarantee which one.

Recommended: Use an Event wrapper

In this solution, you can explicitly manage whether events are handled, thereby reducing errors. The use method is as follows.

/**
 * 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...)
    }
})

The advantage of this solution is that the user needs to specify the intention by using getContentIfNotHandled() or peekContent(). This approach models events as part of the state: they are now just a message that has been consumed or has not been consumed.

To sum up: design events as part of your state. Use your own EventWrapper in the LiveData observer and customize it according to your needs.

In addition, if you have a large number of events, you can use this EventObserver to avoid some duplicate template code.

https://gist.github.com/JoseA...

LiveData with single events

You can search SingleLiveEvent on the Internet to find a good solution for LiveData of one-time events.

The problem

The problem begins, because the LiveData document explains some advantages, which you can find in its document. I list these advantages here by the way.

  • Make sure your user interface matches your data state: LiveData follows the observer mode. When the lifecycle state changes, LiveData will notify the observer object. You can integrate your code to update the UI in these observer objects. Your observer can update the UI every time the application data changes (lifecycle changes), rather than every time there is a change.
  • No memory leaks: the observer is bound to the lifecycle object and cleans itself when its associated lifecycle is destroyed.
  • It will not crash due to the destruction of the Activity: if the observer's life cycle is inactive, such as an Activity in the post stack, it will not receive any LiveData events.

    • There is no need to manually process the life cycle: UI components only observe relevant data, and do not need to actively stop or resume observation. LiveData will automatically manage all this because it knows the relevant lifecycle state changes when observing.
  • Always keep the latest data: if a component's life cycle becomes inactive, it will receive the latest data when it becomes active again. For example, an Activity in the background will receive the latest data immediately after returning to the foreground.
  • Update when configuration changes: if an Activity or Fragment is recreated due to configuration changes, such as device rotation, it will immediately receive the latest available data.
  • Shared resources: you can use singleton mode to extend a LiveData object to wrap system services so that they can be shared in your application. The LiveData object is connected to the system service once, and then any observer who needs the resource can observe the LiveData object. For more information, see extending LiveData.

https://developer.android.com...

However, some of these advantages will not work in all cases, and there is no way to disable them when instantiating LiveData. For example, the feature "always keep the latest data" cannot be disabled, and the main problem to be solved in this paper is how to disable it.

However, I must thank Google for providing the "appropriate configuration change" attribute, which is so useful. But we still need to be able to disable it when we want. I don't need to disable it, but I can let people choose.

The suggested ways to solve the problem

After reading Jose's article, you can find the github source code of the main class of his recommended solution here.

/**
 * 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
}

But a man named feinstein raised two valid questions on this page.

  • Jose's solution lacks support for multiple observers, which is one of LiveData's promises in the name of "shared resources".
  • It is not thread safe.

I can add one more question. By using LiveData, we hope to use the advantages of functional programming in our code, and one of the principles of functional programming is to use immutable data structures. This principle will be broken by the solution recommended by Jose.

After Jose, Kenji tried to solve the problem of "shared resources".

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
    }
}

However, as you can see, internalObserver is passed to the super.observe method once, so it observes the first owner once, the other owners are discarded, and the wrong behavior starts from here. Another bad behavior of this class is that removeObserver does not work as expected, because in the removeObserver method, the instance of internalObserver will be found and it is not in the collection. So nothing will be removed from the collection.

The recommended solution

You can find a standard way to deal with multiple observers in the LiveData class itself, that is, wrap the original observer. Since the LiveData class does not allow us to access its ObserverWrapper class, we must create our version.

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)
        }
    }
}

First, this class is thread safe because the observer property is final and CopyOnWriteArraySet is also thread safe. Secondly, each observer will register with the parent LiveData as its own owner. Third, in the removeObserver method, we want to have an observewrapper. We have registered this observewrapper in the observe method, and we set it in observices to remove it. All this means that we correctly support the shared resource attribute.

Updated 11 / 2018

As a member of my team mentioned, I forgot to deal with the owner in the removeObservers method: lifecycle owner! This may be a problem. If you have multiple Fragments as lifecycle owner and a ViewModel in a page of your application, this may be a problem. Let me correct my solution.

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)
        }
    }
}

In addition to the previous parameters, this is also thread safe because ConcurrentHashMap is thread safe. Here, we should add a hint. You can define the following extensions in your code.

fun <T> LiveData<T>.toSingleEvent(): LiveData<T> {
    val result = LiveEvent<T>()
    result.addSource(this) {
        result.value = it
    }
    return result
}

Then, if you want to have a single event, just call the extension method like this in your ViewModel.

class LiveEventViewModel {
    ...
    private val liveData = MutableLiveData<String>() 
    val singleLiveEvent = liveData.toSingleEvent()
    ...
    ... {
        liveData.value = "YES"
    }
}

And you can use this singleLiveEvent like other livedata.
Pay attention to me and share knowledge every day!

Posted by vladj on Wed, 01 Dec 2021 13:31:57 -0800