LiveData and single events

LiveData and single events

LiveData is great. You just set a value and the View will read that value whenever is needed. If the View is destroyed and then created again (e.g. when changing landscape <-> portrait configurations) the last value will be read.

So that's all, right? Every time we need to send something to the View we use a LiveData<T> and we are done. Well, not exactly.

Since the latest value is read every time the View is created, what happens with things we want to send to the View exactly once? These could be messages shown in a toast, in a dialog, or navigation events. Using just LiveData for these cases would mean that the message/dialog/navigation will happen every time the View is recreated, which is not what we want.

The tricky part is that this might not be obvious because the message/dialog/navigation will work at first. But as soon as you rotate your device and see the message/dialog to re-appear, or you navigate to a screen, go back only to be navigated forward again, you will know that something goes wrong.

So how to pass to the View these single-only events?

Use a wrapper

Maybe the best approach is to wrap your actual data needed for the event (e.g. the String to show in the dialog) in a wrapper that will return the inner value only once. This is to ensure that the value is only consumed once, even if the view is recreated (e.g. on orientation change).

This approach is the official recommended one, but it's quite verbose.

class ValueWrapper<T>(private val value: T) {

    private var isConsumed = false

    fun get(): T? =
        if (isConsumed) {
            null
        } else {
            isConsumed = true
            value
        }
}
The wrapper class
class MyViewModel : ViewModel {

    val toast : LiveData<ValueWrapper<String>>
        get() = toastLiveData

    private val toastLiveData = MutableLiveData<ValueWrapper<String>>()

    fun doSomething() {
        toastLiveData.value = ValueWrapper("Hello world!")
    }
}
The ViewModel
myViewModel.toast.observe(this, Observer {
    it.get()?.let { 
        Toast.makeText(context, this, Toast.LENGTH_LONG).show()
    }
})
The view

Use SingleLiveEvent

Another approach is to use the SingleLiveEvent class. This is a class that was used in a Google example project and became a popular way to handle this issue, even though it was never truly recommended by Google.

The main reason is that if more than one observers are registered, only one will be called (without any way to know which one). If you can live with this, this an alternative, less verbose way, to send events from your ViewModel.

Note that you would need to copy-paste this class into your project. It's not provided in any common library.

class MyViewModel : ViewModel {

    val toast : LiveData<String>
        get() = toastLiveData

    private val toastLiveData = SingleLiveEvent<String>()

    fun doSomething() {
        toastLiveData.value = "Hello world!"
    }
}
The ViewModel
myViewModel.toast.observe(this, Observer {
    Toast.makeText(context, it, Toast.LENGTH_LONG).show()
})
The view

Happy sending values from your ViewModel to your View! :)

Show Comments