zsmb.coAd-free by default



Thoughts about Event Handling on Android

2020.06.09. 18h • Márton Braun

In the previous article of this series, we discussed publishing a ViewState from the ViewModel, which is then observed by the View implementation, rendering content on the screen. However, not everything that the ViewModel has to notify the View about can go in the ViewState.

Why? Because the ViewState is a somewhat permanent, sticky kind of state. For example, if the View goes through a configuration change, and observes the ViewState again, anything in the ViewState will be rendered on the View again. If we want to notify the View about something just once, we can’t place that data in the ViewState, as it may be processed again at a later time.

A frequent use case is notifying the user with a brief, one-time message when an operation completes, whether it was successful or failed. We don’t want this message to reappear every time we put the app in the background and then bring it to the foreground again, or every time we rotate the screen - which is what would happen if we simply stored the information as part of its view state and handled it in the View when it’s rendered.

A notification like above is a typical example of this. Another popular use case would be a trigger for navigation that the View has to handle. We don’t want to place these in the ViewState, because it makes no sense to process either of them more than once.

Navigation is especially fun to attempt to place in the ViewState, since navigating back to a screen stuck with a navigation command as part of its ViewState will keep navigating forwards again!

So there is this new kind of communication from the ViewModel to the View observing it to be considered, where we pass data to the View that’s only processed once. My preferred term for this is events, though you might also see these referred to as view effects sometimes.

This article is part of a series of articles. This one is purely a theoretical introduction for event handling, but this will be put in practice in later articles.

Disclaimer: these are all just subjective thoughts on the topic.

Delivering just once

LiveData works really nicely for storing ViewState, as it’s:

  1. Lifecycle aware: only notifies observers whose lifecycles are in a started state, making it safe to perform actions in a UI component when observing a new value.
  2. Sticky: it holds onto the latest value stored in it, and passes it to new observers as well.

Lifecycle awareness is very handy for events, as processing them when the View isn’t active might not be feasible. However, the stickiness is an issue - that’s exactly what you wouldn’t want for an event.


If approaching this problem with Rx, a BehaviorSubject which would be used for storing state doesn’t work well, as it’s sticky, just like LiveData. A PublishSubject would be a good choice instead, as it will deliver values only to observers that are subscribed to it when the event is emitted, and will only do so once.

However, the lifecycle awareness is missing here - what if the observer is not connected when the event has to be sent, or if it’s connected, but it can’t process it at the moment? For this, UnicastSubject (or UnicastWorkSubject) might be a solution, as it’s able to queue events until an observer processes them.

One stream vs multiple streams

An issue similar to the single/multiple pieces of ViewState comes up when thinking about events. You can either have separate streams of events for each type of event (which, like with state, is what Google tends to do) or a single event stream that all events will be sent to.

Single or multiple streams

Using separate event streams comes with the boilerplate of having to set up a new stream (both creating it and observing it) for every event added to a screen, but works decently otherwise.

When using a single event stream, you’ll need some common supertype for all the events you want to send. Here, you can use an enum to describe your different kinds of events. Or, better yet, you can use a sealed class which would get you all the advantages of enums (checking for them exhaustively using when, and so on) while also allowing you to have different sets of parameters in each event.

If you don’t need exhaustive event handling, introducing a common superclass (or even just a marker interface) for your events can also work.

Queueing events

Let’s take a look at the next important problem of event handling: queueing events as needed.

Queued events

If you’re going down the Rx route and a UnicastSubject works in your implementation, then this is handled already. This contains an internal buffer where values will be placed while they can’t be processed (including when an observer isn’t connected).

However, LiveData, for example, holds onto just a single value at a time. This means that if multiple events are sent and they weren’t processed in time, new incoming events might overwrite previous ones, and only the very last one would be observed when the View becomes an active observer again.

This problem is more prominent if a single event stream delivers every event, but it can also happen if you have separate streams per event type: multiple events of the same type sent before they can be processed would still mean losing some of those events.

Multiple observers

The next thing to consider is multiple observers. Since events are emitted by ViewModels, which may be shared between multiple Fragments, the question of what happens to emitted events when the ViewModel is attached to multiple observers should be posed.

Queued events

The ViewModel should not know about its observers, so it shouldn’t have the ability to direct events to a specific observer. This leaves us with one workable option: all observers should receive every event.

If they don’t all need to process them, the event itself might contain information about which observer should handle it, which they can use to decide whether to act on it.

When dealing with a LiveData based event implementation, this is a significant challenge. Solutions that customize LiveData usually modify it so that it only delivers a value once, which would mean that only a single observer would be notified, causing rather unpredictable behaviour.

Most Rx subjects handle this well by default, as they’ll notify all of their observers of new values sent to them. However, if you’ve opted to use a UnicastSubject for its queueing ability, you’ll run into its limitation of only allowing a single observer at a time. There is probably an Rx based solution for having both queueing and support for multiple observers, but frankly, it’s beyond the scope of my Rx knowledge.

Queue all the things?

Queuing events so that they’re not lost sounds like a good idea, however, in practice, it’s probably not what you’ll want by default.

Placing values in a queue while they haven’t been delivered, and waiting for the observer to become active/connected to deliver them means that there’ll be a delay between the event being sent from the ViewModel and the View processing it. Your expectation when sending an event is most likely that it will be processed now-ish. Even if it doesn’t happen synchronously, and in a blocking way, you probably assume that its delivery is practically instantaneous. However, with queueing in place, observing an event might happen seconds, or even minutes later than sending them.

There are events where it makes sense to process them in the View even if a significant amount of time has passed since sending them, so the ability to queue them is useful. However, for other events, and perhaps by default, it’s a better idea to drop them if they can’t be delivered immediately.

It might also make sense to report the failure to deliver an event as an error, as it was probably posted in the ViewModel with incorrect assumptions about what’s happening in the View at the time. For example, a navigation event was sent to a Fragment that’s not currently the one in the foreground.


Another consideration to make: given multiple observers for your events, you should be careful not to always queue those events for every observer. For example, if multiple observers are capable of handling the same navigation event, you might easily end up with the same navigation being triggered multiple times.

If you have multiple observers for a shared ViewModel, but only one of them is active at a time, then choosing to deliver some events only to the currently active observer might be a solution to this.

Google’s take on events

Google themselves know that there is a need for events, and they have covered it in this article (notably, this was 2 years ago).

They mention SingleLiveEvent, which is implemented by Google like this, as an “okay” option, and note the issue that it will only work with a single observer.

Notably, this SingleLiveEvent implementation is slightly broken, as it creates a new internal Observer instance that it observes itself (meaning the MutableLiveData it extends) with. Therefore, calling removeObserver on it and passing in the original Observer instance doesn’t work - because that Observer is not actually observing the LiveData. Thankfully, the lifecycle used for observing it will correctly clean up the Observer, when it ends.

While that’s deemed okay, the recommendation is something different: an event wrapper. This is a generic Event class that holds the concrete event object inside it, and its observers can either peek at that value, or take it, the latter of which marks the value as handled. This ensures that it’s only handled once.

This is a safe solution as far as the requirement of events being handled once at most goes, though it introduces some boilerplate around creating and handling the events.

It’s not without its issues though:

  • Multiple events of the same kind are not queued, as it’s meant to be used inside a regular MutableLiveData, which only holds the latest value sent to it. If you post multiple events on the same LiveData, some of them might not be delivered.
  • For different types of events, you’ll likely create separate LiveData instances (more boilerplate for pairs of mutable and non-mutable properties, plus setting up their observers).

Finally, the syntax of using these events isn’t great semantically:

_navigateToDetails.value = Event(itemId)

Does this assignment really feel like sending an event? (Additionally, do you really want to type the name of a backing property starting with an _ to send an event?)

Conclusion

That’s a wrap for event handling theory. Hope you’ll consider the problems listed here in your own application’s event handling solutions. To quickly recap them:

  • Making sure to only deliver events once,
  • Choosing between a single stream or multiple streams for events,
  • Supporting multiple observers,
  • Queueing events while they can’t (or shouldn’t) be delivered,
  • Not necessarily queueing every event by default.

If you’ve missed the previous article discussing state handling the same way, catch up on it here: Thoughts about State Handling on Android. Continuing on the state handling topic, you might also be interested in Designing and Working with Single View States on Android.

Stay tuned for upcoming articles, which will get to actually implementing state and event handling.

Thanks to @stewemetal, @Zhuinden, and @itsbata for their input on this article.



You might also like...

Fragment Lifecycles in the Age of Jetpack

Fragments have... Complicated lifecycles, to say the least. Let's take a look at these, and how they all fit into the world of Jetpack today, with LifecycleOwners, LiveData, and coroutines.

Introducing 🌈RainbowCake

RainbowCake is an Android architecture framework, providing tools and guidance for building modern Android applications.

Designing and Working with Single View States on Android

Describing the state of a screen is a common practice these days thanks to MVI popularizing the concept. Let's take a look at some examples of how you can design your state objects neatly using data classes and sealed classes, and how you can put them into practice.

Thoughts about State Handling on Android

Handling the state of UI correctly is one of the prominent challenges of Android apps. Here are my subjective thoughts about some different approaches, which ones I tend to choose, and why.