In this series, I translated a series of articles of coroutines and Flow developers, aiming to understand the reasons for the current design of coroutines, flows and LiveData, find out their problems from the perspective of designers, and how to solve these problems. PLS Enjoy it.

From this article you can see how problems are identified and solved step by step when using LiveData and Flow, especially if you look at these problems from the designer’s point of view, you will learn a general approach to problem solving.

Several years have passed since Jose Alcerreca published his article “The SingleLiveEvent Case”. This article is a good starting point for many developers because it gets them thinking about the different communication modes between ViewModels and related views, whether fragments or activities.

The article can be seen here. Medium.com/androiddeve…

There have been many responses to the SingleLiveEvent case about how to improve the pattern. My favorite article is by Hadi Lashkari Ghouchani article proandroiddev.com/livedata-wi…

However, the two cases mentioned above still use LiveData as an alternative data Store. I think there is still room for improvement, especially when using Kotlin’s Coroutines and Flows. In this article, I’ll describe how I handle one-time events and how to safely observe them throughout the Android life cycle.

Background

In keeping with other articles on SingleliveEvents, or variations that use this pattern, I’ll define events as notifications that take one, and only one, action. The original SingleLiveEvent article used displaying a SnackBar as an example, but you can also use other one-time actions such as navigating with a Fragment, starting an Activity, displaying a notification, etc., as examples of “events”.

In MVVM mode, communication between a ViewModel and its associated view (Fragment or Activity) is usually done by following the observer mode. This decouples the viewmodel from the view, allowing the view to go through various lifecycle states without sending data to the observer.

In my ViewModels, I usually expose two streams for observation. The first is view state. This data flow defines the state of the user interface. It can be observed repeatedly, and is usually supported by Kotlin StateFlow, LiveData, or some other type of data store, exposing a single value. But I will ignore this flow because it is not the focus of this article. However, if you are interested, there are many articles describing how to implement UI state using StateFlow or LiveData.

The second observable flow, the focus of this article, is much more interesting. The purpose of this data flow is to tell the view to perform an action only once. For example, navigate to another Fragment. Let’s explore some of the considerations of this process.

Requirements

It can be said that events are important, even critical. So let’s define some requirements for the process and its observers.

  • New events cannot override events that were not observed.
  • If there is no observer, events must be buffered until the observer starts consuming them.
  • The view may have important lifecycle states during which it can only safely observe events. Therefore, an observer may not always be in the Activity or consumption flow at a particular point in time.

A Safe Emitter of Events

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

What about using SharedFlow? Does that help? Unfortunately, no. SharedFlow is hot. This means that during periods when there are no observers, such as when configuration changes, events emitted into the stream are simply discarded. Unfortunately, this also makes SharedFlow unsuitable for launching events.

So what can we do to meet the second and third requirements? Fortunately, some articles have been written for us.

Roman Elizarov of JetBrains wrote an article about the different uses of various types of traffic.

What’s particularly interesting in this article is the section “A use-case for Channels “, where he describes what we need — A single-event bus, which is A buffered stream of events. This paper addresses the following: elizarov.medium.com/shared-flow…

. Channels also have applications. Channels are used to handle events that must be processed precisely once. This occurs in a design where there is usually one subscriber for one type of event, but intermittently (during startup or some sort of reconfiguration) there is no subscriber at all, and there is a requirement that all published events must be held until a subscriber appears.

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

class MainViewModel : ViewModel() { sealed class Event { object NavigateToSettings: Event() data class ShowSnackBar(val text: String): Event() data class ShowToast(val text: String): Event() } private val eventChannel = Channel<Event>(Channel.BUFFERED) val eventsFlow = eventChannel.receiveAsFlow() init  { viewModelScope.launch { eventChannel.send(Event.ShowSnackBar("Sample")) eventChannel.send(Event.ShowToast("Toast")) }  } fun settingsButtonClicked() { viewModelScope.launch { eventChannel.send(Event.NavigateToSettings) } } }Copy the code

In the example above, the viewmodel fires two events immediately upon construction. The observer may not consume them right away, so they are simply buffered and emitted when the observer starts collecting from the Flow. In the above example, the viewmodel’s handling of button clicks is also included.

The actual definition of an event emitter is surprisingly simple and straightforward. Now that the way events are emitted is defined, let’s move on to discussing how to safely observe these events in the context of Android and the limitations imposed by different lifecycle states.

A Safe Observer of Events

The different life cycles that the Android Framework imposes on developers can be difficult to deal with. Many operations can only be performed safely in certain lifecycle states. For example, Fragment navigation can only take place after onStart but before onStop.

So how can we safely observe the flow of events only in a given lifecycle state? If we look at the stream of events in the viewmodel, say a Fragment, within the coroutine that the Fragment provides, does that satisfy our needs?

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    viewModel.eventsFlow
            .onEach { 
                when (it) {
                    is MainViewModel.Event.NavigateToSettings -> {}
                    is MainViewModel.Event.ShowSnackBar -> {}
                    is MainViewModel.Event.ShowToast -> {}
                }
            }
            .launchIn(viewLifecycleOwner.lifecycleScope)
}
Copy the code

Sadly, the answer is no. ViewLifecycleOwner. LifecycleScope document pointed out that when life cycle is destroyed, the Scope will be cancelled. This means that it is possible to receive events when the life cycle has reached a stopped state but has not yet been destroyed. This can be problematic if operations such as Fragment navigation are performed during event processing.

Pitfalls of using launchWhenX

Perhaps we could use launchWhenStarted to control the different life cycle states in which an event is received? For example.

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // get your view model here lifecycleScope.launchWhenStarted { viewModel.eventsFlow  .collect { when (it) { MainViewModel.Event.NavigateToSettings -> {} is MainViewModel.Event.ShowSnackBar -> {} is MainViewModel.Event.ShowToast -> {} } } } }Copy the code

Unfortunately, there are also major problems, especially with configuration changes. Halil Ozercan has written an excellent in-depth article on Android lifecycle Coroutines, where he describes the basic mechanics behind the launchWhenX set of functions. He points out in the article.

LaunchWhenX functions are not cancelled when the life cycle leaves the desired state. They were just suspended. Cancellation occurs only when the lifecycle reaches the DESTROYED state.

I responded to his article by demonstrating that when looking at a process in any launchWhenX function, it is possible to lose events when configuration changes. The response is long and I won’t repeat it here, so I encourage you to read it.

The address is as follows: themikeferguson.medium.com/pitfalls-of…

Brief presentation about the problem, see gist.github.com/fergusonm/8…

Therefore, unfortunately, we also cannot leverage the launchWhenX extension function to help control what lifecycle state a flow is observed in. So what can we do? To say the least, if we take the time to see what we are doing, we can more easily come up with a solution that is observed only in a particular lifecycle state. Breaking down the problem, we noticed that what we really want to do is start observing in one state and stop observing in another.

If we use another tool, such as RxJava, we can subscribe to the event stream in the onStart lifecycle callback and process it in the onStop callback. A similar pattern can be used for generic callbacks.

override fun onStart() { super.onStart() disposable = viewModel.eventsFlow .asObservable() // converting to Rx for the example .subscribe { when (it) { MainViewModel.Event.NavigateToSettings -> {} is MainViewModel.Event.ShowSnackBar -> {} is MainViewModel.Event.ShowToast -> {} } } } override fun onStop() { super.onStop() disposable? .dispose() }Copy the code

Why can’t we do this with Flow and Coroutines? Well, we can. When the life cycle is broken, the scope is still cancelled, but we can shrink the time the observer is in the Activity state to only the life cycle state between start and stop.

override fun onStart() { super.onStart() job = viewModel.eventsFlow .onEach { when (it) { MainViewModel.Event.NavigateToSettings -> {} is MainViewModel.Event.ShowSnackBar -> {} is MainViewModel.Event.ShowToast -> {} } } .launchIn(viewLifecycleOwner.lifecycleScope) } override fun onStop() { super.onStop() job? .cancel() }Copy the code

This satisfies the third requirement and solves the problem of observing the flow of events only in the secure lifecycle state, but it introduces a large number of templates.

Cleaning Things Up

What if we delegated the responsibility of managing this work to something else to help eliminate these templates? Patrick Steiger’s article “replacing StateFlow or SharedFlow with LiveData” has a surprising interlude. (This is also a good read). The original address is proandroiddev.com/should-we-c…

He created a set of extension functions that automatically subscribe to a traffic Collect when the owner of a lifecycle reaches the start and unsubscribe when the lifecycle reaches the stop phase. Here is my slightly modified version.

(Edited October 2021: See the updated version below, which takes advantage of recent library changes.)

class FlowObserver<T> ( lifecycleOwner: LifecycleOwner, private val flow: Flow<T>, private val collector: suspend (T) -> Unit ) { private var job: Job? = null init { lifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { source: LifecycleOwner, event: Lifecycle.Event -> when (event) { Lifecycle.Event.ON_START -> { job = source.lifecycleScope.launch { flow.collect { collector(it) } } } Lifecycle.Event.ON_STOP -> { job? .cancel() job = null } else -> { } } }) } } inline fun <reified T> Flow<T>.observeOnLifecycle( lifecycleOwner: LifecycleOwner, noinline collector: suspend (T) -> Unit ) = FlowObserver(lifecycleOwner, this, collector) inline fun <reified T> Flow<T>.observeInLifecycle( lifecycleOwner: LifecycleOwner ) = FlowObserver(lifecycleOwner, this, {})Copy the code

Using these extensions is super simple and straightforward.

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

        viewModel.eventsFlow
                .onEach {
                    when (it) {
                        MainViewModel.Event.NavigateToSettings -> {}
                        is MainViewModel.Event.ShowSnackBar -> {}
                        is MainViewModel.Event.ShowToast -> {}
                    }
                }
                .observeInLifecycle(this)
    }

// OR if you prefer a slightly tighter lifecycle observer:
// Be sure to use the right lifecycle owner in each spot.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    viewModel.eventsFlow
            .onEach {
                when (it) {
                    MainViewModel.Event.NavigateToSettings -> {}
                    is MainViewModel.Event.ShowSnackBar -> {}
                    is MainViewModel.Event.ShowToast -> {}
                }
            }
            .observeInLifecycle(viewLifecycleOwner)
}
Copy the code

Now we have an event observer that only observes after the start life cycle is reached, and cancels when the stop life cycle is reached.

It has the added benefit of restarting Flow Collect when the lifecycle transition from stop to start is less common, but not impossible.

This makes it safe to perform operations such as Fragment navigation or other life-cycle-sensitive processing without worrying about what the state of the life cycle is. Flow is collected only in a safe lifecycle state!

Pulling It All Together

Putting everything together, this is the basic pattern I use to define the “single live event” flow, and how I can safely observe it.

To summarize: The flow of events in the viewmodel is defined by a channel receive as a flow. This allows the viewmodel to submit events without knowing the state of the observer. The event is buffered in the absence of an observer.

A view (that is, a Fragment or Activity) only observes the flow after its lifecycle reaches the start state. When the life cycle reaches a stop event, the observation is cancelled. This allows events to be handled safely without worrying about the difficulties of the Android life cycle.

Finally, with the help of FlowObserver, the template was eliminated.

You can see the entire code here.

class MainViewModel : ViewModel() { sealed class Event { object NavigateToSettings: Event() data class ShowSnackBar(val text: String): Event() data class ShowToast(val text: String): Event() } private val eventChannel = Channel<Event>(Channel.BUFFERED) val eventsFlow = eventChannel.receiveAsFlow() init  { viewModelScope.launch { eventChannel.send(Event.ShowSnackBar("Sample")) eventChannel.send(Event.ShowToast("Toast")) }  } fun settingsButtonClicked() { viewModelScope.launch { eventChannel.send(Event.NavigateToSettings) } } } class MainFragment : Fragment() { companion object { fun newInstance() = MainFragment() } private val viewModel by viewModels<MainViewModel>() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState: Bundle?) : View { return inflater.inflate(R.layout.main_fragment, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Note that I've chosen to observe in the tighter view lifecycle here. // This will potentially recreate an observer and cancel it as the // fragment goes from onViewCreated through to onDestroyView and possibly // back to onViewCreated. You may wish to use the "main" lifecycle owner // instead. If that is the case you'll need to observe in onCreate with the // correct lifecycle. viewModel.eventsFlow .onEach { when (it) { MainViewModel.Event.NavigateToSettings -> {} is MainViewModel.Event.ShowSnackBar -> {} is MainViewModel.Event.ShowToast -> {} } } .observeInLifecycle(viewLifecycleOwner) } }Copy the code

I would like to commend all the authors mentioned in this article. Their contribution to the community has greatly enhanced the quality of my work.

Errata

Edited March 2021

It’s been a few months since I published this article. Google has provided new tools (still in alpha) that provide solutions similar to those I write about below. You can read it here.

Medium.com/androiddeve…

Edited in October 2021

With androidx.lifecycle updated to version 2.4, you can now use flowWithLifecycle or repeatWithLifecycle extension functions instead of the one I defined above. For example.

viewModel.events
    .onEach {
        // can get cancelled when the lifecycle state falls below min
    }
    .flowWithLifecycle(lifecycle = viewLifecycleOwner.lifecycle, minActiveState = Lifecycle.State.STARTED)
    .onEach {
        // Do things
    }
    .launchIn(viewLifecycleOwner.lifecycleScope)
Copy the code

You can also do the same manually with repeatWithLifecycle. There are a bunch of different ways to extend functions to make them more readable. Here are two of my favorites, but there are many variations.

inline fun <reified T> Flow<T>.observeWithLifecycle(
        lifecycleOwner: LifecycleOwner,
        minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
        noinline action: suspend (T) -> Unit
): Job = lifecycleOwner.lifecycleScope.launch {
    flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect(action)
}

inline fun <reified T> Flow<T>.observeWithLifecycle(
        fragment: Fragment,
        minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
        noinline action: suspend (T) -> Unit
): Job = fragment.viewLifecycleOwner.lifecycleScope.launch {
    flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle, minActiveState).collect(action)
}
Copy the code

There is also a use example with any minimal Activity state.

viewModel.events
    .observeWithLifecycle(fragment = this, minActiveState = Lifecycle.State.RESUMED) {
        // do things
    }

viewModel.events
    .observeWithLifecycle(lifecycleOwner = viewLifecycleOwner, minActiveState = Lifecycle.State.RESUMED) {
        // do things
    }
Copy the code

The original link: proandroiddev.com/android-sin…

I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit