Photo by Phil Goodwin on Unsplash

Android SingleLiveEvent Redux with Kotlin Flow

Michael Ferguson
ProAndroidDev
Published in
10 min readJan 26, 2021

--

December 2021 Note: It’s been a year since this article was first written. In that time Google has updated their official guidance on the “single live event” problem. I’ve added an addendum to the bottom of this article that links to it.

I recommend you use their new guidance. While channels may appear to work under light work loads there are some issues with guaranteed delivery with channels that were discovered after I originally wrote this article. See: https://github.com/Kotlin/kotlinx.coroutines/issues/2886 (As an aside, a nice, though non-flow, work around is documented in that issue too!)

The most recent guidance is here:

Below is my original article. It might be worth a read as contextual background.

Several years have passed since Jose Alcérreca published his article on “The SingleLiveEvent Case”. This article was a great launching point for many developers as it got them thinking about different communication patterns between ViewModels and the associated View, be it a fragment or an activity.

There have been many responses to the SingleLiveEvent case about ways to improve upon the pattern. One of my favorites was written by Hadi Lashkari Ghouchani.

However, both those cases still used LiveData as a backing data store. I feel there is still room to improve, especially with the use of Kotlin coroutines and flows. In this article I will describe how I handle one-shot events and how to observe those events safely within the Android lifecycle.

Background

To stay consistent with other articles about SingleLiveEvent, or variations of that pattern, I’m going to define an event as a notice to take an action once and once only. The original SingleLiveEvent article used displaying a SnackBar as an example but you can include other one-shot action such as fragment navigation, starting an activity, displaying a notification and so on as examples of “events”.

In the MVVM pattern, the communication between the ViewModel and its associated view (fragment or activity) is often done by following the observer pattern. This decouples the view model from the view allowing the view to go through various lifecycle states independently of data being emitted to the observers.

In my ViewModels, I usually expose two flows to be observed. The first is the view state. This flow defines what the state of the UI is. It can be observed repeatedly and is usually backed by a Kotlin StateFlow LiveData or some other type of data store that exposes a single value. I’m going to ignore this flow as it is not the focus of this article. However, if are you interested there are many articles that describe how to implement UI state with StateFlow or LiveData.

The second observable flow, and the focus of this article, is much more interesting. The purpose of this flow is notify the view to perform an action once and once only. For example, to navigate to another fragment. Let’s explore what options we have for this flow.

Requirements

Presumably event are important, even critical. So lets define some requirements for this flow and its observer:

  1. New events cannot overwrite unobserved events.
  2. If there is no observer, events must buffer until an observer starts to consume them.
  3. The view may have important lifecycle states during which it can only observe events safely. Thus the observer may not always be active or consuming the flow at a given moment in time.

A Safe Emitter of Events

So satisfy the first requirement, it’s clear that a stream is necessary. LiveData or any Kotlin flow that conflates values, such as StateFlow or a ConflatedBroadcastChannel, is not appropriate. A set of rapidly emitted events may overwrite each other with only the last event being emitted to the observer.

What about the use of SharedFlow? Can that help? Unfortunately, no. SharedFlow is hot. This means that during periods where the is no observer, say during a configuration change, events emitted on to the flow are simply dropped. Regrettably, this also makes SharedFlow inappropriate to emit events on.

So what options do we have to meet the second and third requirements? Fortunately, some options have already been described for us. Roman Elizarov of JetBrains wrote a great article on the different use cases for various types of flows.

Of particular interest in that article is the section “A use-case for channels” where he describes exactly what we need - a single-shot event bus that is a buffered stream of events.

From the article (emphasis mine):

… channels also have their application use-cases. Channels are used to handle events that must be processed exactly once. This happens in a design with a type of event that usually has a single subscriber, but intermittently (at startup or during some kind of reconfiguration) there are no subscribers at all, and there is a requirement that all posted events must be retained until a subscriber appears.

Now that we’ve found a safe way to emit events, let us define the basic structure of an example ViewModel with some sample events.

The above example view model emits two events immediately on construction. The observer may not be there to consume them right away so they are simply buffered and emitted when the observer starts to collect from the flow. Also included in the above example is the view model reacting to a button click.

The actual definition of the event emitter is surprisingly simple and straightforward. Now that the way events will be emitted is defined let's move on to how we can safely observe these events in the context of Android and the constraints imposed by the different lifecycle states.

A Safe Observer of Events

The different lifecycles the Android framework imposes on developers can be difficult to work with. Many actions may only be safely performed within certain lifecycle states. For example, fragment navigation should only be done after onStart but before onStop.

So how do we safely observe the flow of events only within given lifecycle states? If we observe the view model event flow from, say a fragment, within the coroutine scopes provided by the fragment does this do what we need?

Unfortunately the answer is no. The documentation for viewLifecycleOwner.lifecycleScope indicates that this scope is cancelled when the lifecycle is destroyed. This means that it’s possible to receive events after the lifecycle has reached the stopped state but is not yet destroyed. This may be problematic if actions, such as fragment navigation, are performed as part of processing of the event.

Pitfalls of usinglaunchWhenX

Perhaps we can use launchWhenStarted to control the different lifecycle states an event is received in? For example:

Unfortunately this has some major issues as well, particularly with respect to configuration changes. Halil Ozercan wrote a wonderful deep dive into the Android Lifecycle Coroutines where he describes the underlying mechanisms behind the launchWhenX set of functions.

He notes in his article:

launchWhenX functions are not cancelled when lifecycle leaves the desired state. They simple get paused. Cancellation happens only if lifecycle reaches DESTROYED state.

I responded to his article with a demonstration that it is possible to lose events on configuration change when observing a flow from within any of the launchWhenX functions. It’s a long enough response that I won’t repeat it here so I encourage you to go read it.

Edit: For a concise demonstration of this see https://gist.github.com/fergusonm/88a728eb543c7f6727a7cc473671befc

Thus, regrettably, we cannot take advantage of the launchWhenX extension functions either to help control what lifecycle state a flow is observed in. So what can we do?

Taking a Step Back

If we take a moment to look at what we’re trying to do we can more easily figure out a solution to observing only in specific lifecycle states. Breaking down the problem we note that that all we really want to do is start observing in one state, and stop observing in another.

If we were using another tool, say RxJava, we might subscribe to the event stream in on onStart lifecycle callback, and dispose during the onStop callback. (A similar pattern could be required for generic callbacks as well.)

Why can’t we do that with Flow and coroutines? Well, we can. The scope will still cancel when the lifecycle is destroyed but we can tighten up when the observer is active to just the lifecycle states between start and stop.

This meets the third requirement and solves the problem of only observing the event stream during safe lifecycle state but it introduces a lot of boilerplate.

Cleaning Things Up

What if we delegate the responsibility of managing the job to something else to help remove that boilerplate?

Patrick Steiger’s article on substituting LiveData for StateFlow or SharedFlow had an amazing little nugget in it. (It’s also a great read.)

He created a set of extension functions that automatically subscribes a flow collector when a lifecycle owner reached start, and cancels the collector when the lifecycle reached the stop phase!

Below is my slightly modified version of it:

(October 2021 edit: See below for an updated version that makes use of more recent library changes.)

Using these extension functions is super simple and straightforward.

Now we have an event observer that observes only after the start lifecycle has been reached and it cancels when the stop lifecycle has been reached.

It also has the added benefit of re-starting flow collection when the lifecycle goes through a less common, but not impossible, transition from stop to start.

This makes it safe to perform actions like fragment navigation or other lifecycle sensitive processing without worrying about what the lifecycle state is. The flow is only collected during safe lifecycle states!

Pulling It All Together

Bringing everything together this is the basic pattern I use to define a stream of “single live events” and how I observe it safely.

To summarize: the view model’s event stream is defined using a channel receiving as a flow. This allows the view model to submit events without having to know about the state of the observer. Events are buffered when there is no observer.

The view (ie. the fragment or the activity) is observing that flow only after the lifecycle has reached the started state. Observation is cancelled when the lifecycle reaches the stopped event. This allows safe processing of events without worrying about the difficulties imposed by the Android lifecycle.

Finally, with the help of the FlowObserver the boilerplate is eliminated.

You can see the entire sample here:

I want to give a huge shout out to all the authors referenced in this article. Their contributions to the community has greatly improved the quality of my work.

Errata

I want to give a huge shout out to George for pointing out a bug I had in my implementation. He has a discussion on his gist with his implementation of the same issue. He brings up a few other details that are worth exploring.

Thanks again for the comments George!

March 2021 Edit

It’s been a few months since I published this article. Google has provided new tooling (still in the alpha state) that provides similar solutions to what I wrote below. You can read about it here:

October 2021 Edit

With updates to androidx.lifecycle to version 2.4 you can now use the flowWithLifecycle or repeatWithLifecycle extension function rather than the one I defined above.

For example:

You can also manually do the same thing with repeatWithLifecycle. There’s a ton of different ways to make that more readable with extension functions. Below my two favourites but there are plenty of variations:

And a usage example with an arbitrary minimum active state:

December 2021 Edit

Google’s updated their guidance on how to handle the “single live event” problem yet again. You can find the updated guidance here: https://developer.android.com/jetpack/guide/ui-layer/events#other-use-cases

In short, their recommendation is for the View to call back to the ViewModel to inform it that a single live event / action has been processed. Eg. the view would call the view model to notify it that a snackbar has been displayed or that a fragment navigation command has been processed.

I’m not sure I entirely agree with their recommendations, in particular with respect to navigation, but maybe it’s just new enough to still be uncomfortable. It is a solution that eliminates a number of lifecycle issues. I can see where they are coming from, especially as it relates to Compose and the unidirectional data flow mental model of “remembering” state and pushing events back up to notify when changes happen.

(Sneaky March 2022 edit: Actually, https://github.com/Kotlin/kotlinx.coroutines/issues/2886 why it’s not a good idea anymore.)

Consume the events whenever you want, just let the view model know when they are consumed. Events can be re-observed as many times as you like, by as many observers as you like. But, it’s the view’s responsibility to ensure that the view model is aware the event has been consumed. It works, but seems verbose to me. Perhaps I’ll write up an article on how to work with that model some day.

June 2022 Edit: yet more new guidance from Google:

References

https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

https://proandroiddev.com/livedata-with-single-events-2395dea972a8

https://elizarov.medium.com/shared-flows-broadcast-channels-899b675e805c

https://medium.com/swlh/deep-dive-into-lifecycle-coroutines-e7192312faf

https://themikeferguson.medium.com/pitfalls-of-observing-flows-in-launchwhenresumed-2ed9ffa8e26a

https://medium.com/androiddevelopers/viewmodel-one-off-event-antipatterns-16a1da869b95

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Michael Ferguson

Android software developer. Views and opinions expressed are my own and not that of my employer.

Responses (12)

Write a response