• LiveData with SnackBar, Navigation and other Events (The SingleLiveEvent Case)
  • Jose Alcerreca
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: wzasd
  • Proofread by: LeeSniper

A convenient way for the Activity or Fragment layer to communicate with the ViewModel layer is to use LiveData for observations. This view layer subscribes to and reacts to Livedata data changes. This applies to data that is continuously displayed on the screen.

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

This should be seen as a design problem, rather than trying to solve it through libraries or extensions of architectural components. We recommend that you treat your events as part of your status. In this article, we’ll show you some common ways to go wrong, along with recommended ways.

❌ Error: 1. Use LiveData to resolve events

This method holds Snackbar messages or navigation information directly inside a LiveData object. Although in principle it looks like normal LiveData objects could be used here, there are some problems.

In a master/slave application, here is the master ViewModel:

// Do not use this event class ListViewModel: ViewModel { private val _navigateToDetails = MutableLiveData<Boolean>() val navigateToDetails : LiveData<Boolean> get() = _navigateToDetails funuserClicksOnButton() {
        _navigateToDetails.value = true}}Copy the code

At the view layer (Activity or Fragment) :

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

The problem with this approach is that the values in _navigateToDetails remain true for a long time and cannot be returned to the first screen. Step by step analysis:

  1. The user clicks the button to start the Details Activity.
  2. User The user presses back to return to the main Activity.
  3. The observer goes from the non-listening state to the listening state again when the Activity is on the fallback stack.
  4. But the value is still true, so the Detail Activity fails to start.

The solution is to set the navigation flag from the ViewModel to false immediately after it is clicked;

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

However, it is important to remember that LiveData stores this value, but does not guarantee to emit every value it receives. For example, you can set a value when no observer is listening, so the new value will replace it. In addition, setting values from different threads can result in resource contention, signaling only one change to the observer.

But the main problem with this approach is that it is difficult to understand and concise. How do we ensure that values are reset after a navigation event occurs?

❌ is probably better: 2. Use LiveData for event handling, resetting the initial value of the event in the observer

With this approach, you can add a way to tell from the view that you have handled the event and reset the event.

usage

By making a few small changes to our observer, we have this solution:

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

Add a new method to the ViewModel like this:

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

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


    fun userClicksOnButton() {
        _navigateToDetails.value = true
    }

    fun navigateToDetailsHandled() {
        _navigateToDetails.value = false}}Copy the code

The problem

The problem with this approach is that it’s a bit rigid (each event has a new method in the ViewModel) and error-prone, and it’s easy for observers to forget to call the ViewModel method.

✔️ Correct solution: Use SingleLiveEvent

The SingleLiveEvent class is intended to be a solution for a particular scenario. This is a LiveData that only sends one update.

usage

class ListViewModel : ViewModel {
    private val _navigateToDetails = SingleLiveEvent<Any>()

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


    fun userClicksOnButton() {
        _navigateToDetails.call()
    }
}
Copy the code
myViewModel.navigateToDetails.observe(this, Observer {
    startActivity(DetailsActivity...)
})
Copy the code

The problem

The problem with SingleLiveEvent is that it is limited to one observer. If you inadvertently add more than one, only one will be called, and you cannot guarantee which one.

✔️ Recommended: Use event wrappers

In this approach, you can explicitly manage whether events have been handled, thereby reducing errors.

usage

/**
 * 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 }Copy the code
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
    }
}
Copy the code
myViewModel.navigateToDetails.observe(this, Observer { it.getContentIfNotHandled()? .let { // Only proceedifthe event has never been handled startActivity(DetailsActivity...) }})Copy the code

The advantage of this approach is that the user uses getContentIfNotHandled() or peekContent() to specify the intent. This approach models events as part of the state: they are now just a consumed or unconsumed message.

Using the event wrapper, you can add multiple observers to a one-time event.


Bottom line: Design events as part of your state. Use your own event wrapper and customize it to your needs.

Silver bullet! If you end up with a lot of events, use this EventObserver to remove a lot of useless code.

Thanks to Don Turner, Nick Butcher, and Chris Banes.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.