LiveData has become an important part of many Android apps. With it’s elegant handling of the complex lifecycle inherent in many Android components, its understandable that we would try to use them everywhere we can.

LiveData, however, was designed with a very specific use case in mind. Its intended to hold data for use by views in an application, and make that data available in a lifecycle-safe fashion, while helping us reduce the need to reload data unnecessarily.

All of this makes LiveData a great fit for storing and delivering ViewStates, but there is another type of information that view models need to deliver to views for which LiveData is a bit less well suited: events.

View Events

While the view state contains data that’s important for the view to be able to display to the user, view events are transient events that the view model sends to the view. These can be things like notifications that might be shown in a SnackBar, or items that have been selected for navigation. The key difference is that view state should be saved and restored, while view events should not.

For example, my app Pigment has a screen that displays pages that the user can color. If the user has already started coloring a page, their project thumbnail is shown instead of the generic thumbnail, and tapping on the page resumes their previous coloring session.

When the user selects a page the view model needs to:

  1. Check for an existing project for the page, and open it, if one exists.
  2. Check if the user can access the page because it’s free or they’re a subscriber.
  3. If not, display the page unlock dialog.
  4. If so, download the page, create a new project, and open it for the user.
  5. If there are any errors along the way (like download errors), inform the user.

The result of this process can be one of three events: either open a project, show the page unlock dialog, or display an error. All of these events should be handled once and they should never be restored.

This is some relatively complex logic that should live in the view model, where its easy to test, but how should the resulting events be communicated from the view model to the view?

Event Wrapper

There’s been a lot of discussion to try to find ways to make LiveData fit as a solution for this use case. The problem with storing these transient events in LiveData in the view model is that they will emit the event when the view is restored. This can be problematic, especially in the case of navigation events, since the user will be stuck in an infinite loop, never able to go back.

The suggested way to handle these single use events is with an Event wrapper. This is a simple class that holds a consumable value, only providing it a single time.

class Event<out T>(private val content: T) {
  private var hasBeenHandled = false

  fun getContentIfNotHandled(): T? = if (hasBeenHandled) {
    null
  } else {
    hasBeenHandled = true
    content
  }
}

This allows us to wrap arbitrary data in a self destructing envelope and include it in our view state.

data class PageListViewState(
  val pages: List<PageListViewState.Page>,
  val selectedProject: Event<Project>?,
  val showUnlockDialog: Event<Page>?,
  val pageLoadError: Event<Page>?
)

When consuming our view state, we simply check if the result of selectedProject?.getContentIfNotHandled() is not null then we need to handle the event. When the view is restored, and the view state with it, the second call to getContentIfNotHandled() will return null, since the value has been consumed, meaning the event won’t be handled a second time.

The problem with this approach is that our view state is now mutable. While we might have wrapped the mutability in a class so that the view state itself appears immutable, the contents of the Event class changes when it’s read, leaving the potential for complex bugs that can be avoided.

ViewEvent

A better solution is to separate these transient events from the view state and deliver them independently. Since the events that a view model can emit are mutually exclusive, and views support a finite set of events, they are a great candidate for a sealed class.

A sealed class in Kotlin is a class which has a restricted hierarchy, meaning it can be subclassed, but only by a fixed set of subclasses. This allows us to represent our fixed set of events, and be sure that we’re handling all events with the use of a when statement, so that the compiler helps make sure we handle all possible events.

sealed class PageListViewEvent
data class SelectedProject(val project: Project) : PageListViewEvent()
data class SelectedLockedPage(val page: Page) : PageListViewEvent()
object PageLoadFailed : PageListViewEvent()

Notice that SelectedProject and SelectedLockedPage both contain data that’s needed to process the event, but PageLoadFailed doesn’t have any associated data. When events require data, we can use a data class, otherwise we can make them an object. This helps reduce the complexity of the objects.

Now all of the events our view model can emit are subclasses of PageListViewEvent, and they each contain the relevant data to be acted upon. Now we just need to deliver them in a way that ensures they won’t be saved and restored along with our view state.

Delivering Events

Since we already discussed how LiveData isn’t a good fit for events, an alternative approach is to expose the events via an RxJava PublishRelay. This is an observable producer that doesn’t keep any state, but delivers elements only when they’re supplied.

Note: We could also use a PublishSubject, but a Relay ensures that it’s stream is never closed via onComplete or onError, making them a little safer.

class PageListViewModel : ViewModel() {

  private val _viewEvents = PublishRelay<PageListViewEvent>
  val viewEvents: Observable<PageListViewEvent> = _viewEvents

  private suspend fun downloadPage(page: Page) {
    try {
      val asset = download(page.asset)
      val project = createProject(page, asset)
      _viewEvents.onNext(SelectedProject(project))
    } catch (e: Exception) {
      _viewEvents.onNext(PageLoadFailed)
    }
  }
}

Now we call _viewEvents.onNext() with any events that we want to emit and they’ll be delivered to the view without ever being saved as part of our state.

Lifecycle Aware Observation

As I mentioned earlier, one of the benefits of LiveData is it’s Lifecycle awareness, which ensures that its not observing data when the observing component is in an invalid state. RxJava doesn’t have this built in, but the AutoDispose library from Uber offers us similar functionality by automatically managing our observable subscriptions.

Using AutoDispose, we can observe the view model’s ViewEvents similarly to how we observe the ViewState.

class PageListActivity : Activity() {

  fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(bundle)

    // Observe the viewState LiveData
    viewModel.viewState.observe(this, this::onViewState)

    // Observe the viewEvents Observable
    viewModel.viewEvents
      .observeOn(AndroidSchedulers.mainThread())
      .autoDisposable(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
      .subscribe {
        when (it) {
          is SelectedProject -> navigateToEditor(it.project))
          is SelectedLockedPage -> showUnlockDialog(it.page)
          PageLoadFailed -> showError()
        }.exhaustive
      }
  }

  // Extension function that turns a when statement into an expression
  // so the compiler enforces handling of all states.
  private val <T> T.exhaustive: T get() = this
}

Now we have the lifecycle awareness of LiveData without the state saving, making this a perfect fit for transient events.

Conclusion

While it’s convenient for state, not everything communicated between a view model and view should be saved and restored. The existing solutions that try to pack transient events into LiveData require giving up a core value of ViewState, that it should be immutable, and add complexity to our apps.

ViewEvents allow us to easily encapsulate all of the events that a view model can emit in a clean container, and PublishRelays mixed with AutoDispose provide lifecycle aware observation like LiveData, without the saving and restoration.

What do you think? Do you deliver transient events with LiveData, or is this something you’d like to try out? Let me know your thoughts, or questions or comments, by tweeting @rharter.

Thanks to Nate Ebel for reviewing a draft of this post.