3

I was looking at the JetSurvey project (Android Jetpack Compose sample project) and noticed that they created a class to wrap the LiveData value in their ViewModel class. Here is the class I'm talking about:

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
data 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
}

In the ViewModel, this is used like so:

private val _navigateTo = MutableLiveData<Event<Screen>>()
    val navigateTo: LiveData<Event<Screen>> = _navigateTo

fun signInAsGuest() {
        _navigateTo.value = Event(Survey)
    }

It seems the point of the Event class is to avoid the navigation from happening multiple times. However, I don't understand how that would happen in the first place, because the navigation would only get triggered once the LiveData value is updated. And every time the value is updated, a new Event object is created, so it would run again.

So in the Fragment, is there a chance that the code inside viewModel.navigateTo.observe(viewLifecycleOwner) can run multiple times without the value having been updated? If so, in what cases would that happen?

If my understanding of the role of the Event wrapper is incorrect, for what purpose is it being implemented? Is it necessary at all?

ctkim
  • 67
  • 6

1 Answers1

2

Suppose your LiveData observer in a Fragment navigates to a second Fragment. The user rotates the screen so the first Fragment instance is destroyed. When they back out of the second Fragment, a new instance of the first Fragment is recreated, so its observer is triggered again. Without the event wrapper, it would suddenly and surprisingly navigate back to the second Fragment immediately.

Also, a LiveData might have multiple observers. Maybe two different Fragments are observing the same events LiveData, but you don't want to risk showing the user a message twice for the same event. For example, they could navigate to a second Fragment that is observing the same events LiveData as the first Fragment. An event fires and the second Fragment observer shows the user a dialog box or something. Then the user backs up to the first Fragment and that same event will fire the first Fragment's observer so the event gets handled twice.

If your project uses coroutines, a cleaner solution for this event observing problem is to use a SharedFlow with replay of 0 instead of using a LiveData with an event wrapper class. I suspect the reason they didn't use SharedFlow in the JetSurvey project is that they don't want to assume you're already familiar with Flows when that's not what the example is about.

An alternate solution called SingleLiveEvent appeared in an official Android example once, but was considered too hacky to add to the Jetpack libraries.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • What if I have a toggle that switches between 2 enums and I want to show a Toast based on the value, I cannot use SharedFlow because it won't store the last state. – David Jul 10 '22 at 07:31
  • You could use a private Boolean property with it. Or you could use a private `MutableStateFlow`, and expose a public SharedFlow using `shareIn` on that MutableStateFlow. – Tenfour04 Jul 10 '22 at 11:50
  • OK, using shareIn did not work at the end because you have the issue that when you first call collect you already get a even triggered. – David Jul 11 '22 at 00:35
  • Maybe you can open your own question that describes exactly what your use case is. I don't really understand. – Tenfour04 Jul 11 '22 at 01:38