In this series, I translated a series of articles written by the developers of collaborative process and Flow, in order to understand the reasons for the current design of collaborative process, Flow and LiveData, find their problems from the perspective of designers and how to solve them. pls enjoy it.
❝ from this article, you can learn how we find and solve problems step by step when using LiveData and Flow. Especially from the perspective of designers, you will learn the general methods to solve problems. ❞
Several years have passed since Jose Alc é rreca published his article "SingleLiveEvent Case". This article is a good starting point for many developers because it allows them to think about the different communication modes between ViewModels and related views (whether Fragment or Activity).
This article can be read here. https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
For the SingleLiveEvent case, there have been many responses on how to improve the pattern. One of my favorite articles is an article by Hadi Lashkari Ghouchani https://proandroiddev.com/livedata-with-single-events-2395dea972a8
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 flow. In this article, I will describe how I handle one-time events and how I can safely observe them throughout the Android lifecycle.
Background
In order to be consistent with other articles on SingleLiveEvent, or variants using this pattern, I will define an event as a notification that takes one and only one action. The original SingleLiveEvent article takes displaying SnackBar as an example, but you can also take other one-time actions, such as Fragment navigation, starting Activity, displaying notifications, etc. as examples of "events".
In MVVM mode, the communication between ViewModel and its related views (fragments or activities) is usually completed by following observer mode, which decouples the view model from the view and allows the view to experience various life cycle states without sending data to the observer.
In my ViewModels, I usually expose two streams for observation. The first is the 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 other types of data stores, exposing a single value. However, I will ignore this process because it is not the focus of this article. However, if you are interested, there are many articles describing how to implement UI state with StateFlow or LiveData.
The second observable stream, which is also the focus of this article, is much more interesting. The purpose of this data flow is to inform the view to perform an action only once. For example, navigate to another Fragment. Let's explore what needs attention in 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 overwrite unobserved events.
- If there is no observer, events must be buffered until the observer begins to consume them.
- A view may have important lifecycle states during which it can only safely observe events. Therefore, the observer may not always be on an Activity or consumption stream at a particular point in time.
A Safe Emitter of Events
Therefore, to meet the first requirement, it is obvious that a flow is necessary. LiveData or any conflates Kotlin flow, such as StateFlow or ConflatedBroadcastChannel, are not appropriate. A set of fast launching events may cover each other, and only the last event is launched to the observer.
What about using SharedFlow? Can this help? Unfortunately, No. SharedFlow is hot. This means that when there is no observer, such as when the configuration changes, the events emitted into the stream will be 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 described for us.
Roman Elizarov of JetBrains wrote an article about the different uses of various types of traffic.
What is particularly interesting in this article is the "a use case for channels" section, which describes what we need - a single event bus, which is a buffered event flow. The article address is as follows: https://elizarov.medium.com/shared-flows-broadcast-channels-899b675e805c
❝...... channels also has its applications. channels are used to handle events that must be handled exactly once. This occurs in a design. There is a type of event that usually has a subscriber, but intermittently (during startup or some reconfiguration) there is no subscriber at all, and there is a requirement that all published events must be retained until a subscriber appears. ❞
Now that we have found a safe way to emit events, let's use some sample events to define the basic structure of a ViewModel.
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) } } }
In the above example, two events were fired immediately when the view model was built. The observer may not consume them immediately, so they are simply buffered and emitted when the observer starts collect ing from the Flow. In the above example, the processing of button clicking in the view model is also included.
The actual definition of event emitters is surprisingly simple and straightforward. Now that the launch mode of events has been defined, let's continue to discuss how to safely observe these events in the context of Android and the limitations brought by different lifecycle states.
A Safe Observer of Events
The different lifecycles imposed on developers by the Android Framework can be difficult to deal with. Many operations can only be performed safely in certain lifecycle states. For example, Fragment navigation can only be performed after onStart and before onStop.
So how can we safely observe the flow of events only in a given lifecycle state? If we observe the event flow of the view model, such as a Fragment, within the coroutine provided by the Fragment, can this meet 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) }
Unfortunately, the answer is No. viewLifecycleOwner. The lifecyclescope documentation indicates that this Scope will be cancelled when the lifecycle is destroyed. This means that it is possible to receive events when the life cycle reaches a stop state but has not been destroyed. This can be problematic if you perform operations such as Fragment navigation during event processing.
Misunderstanding of using launchWhenX
Maybe we can use launchWhenStarted to control the different lifecycle states in which an event is received? for instance.
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 -> {} } } } }
Unfortunately, there are also some major problems, especially in terms of configuration changes. Halil Ozercan wrote a wonderful in-depth article on Coroutines in the Android life cycle. He described the basic mechanism behind the launchWhenX set of functions. He pointed out in his article.
❝ the launchWhenX function is not cancelled when the lifecycle leaves the desired state. They're just suspended. Only when the lifecycle reaches the DESTROYED state will it be cancelled. I responded to his article and proved that when observing a process in any launchWhenX function, it is possible to lose events when the configuration changes. This response is very long, so I won't repeat it here, so I encourage you to read it. ❞
The address is as follows: https://themikeferguson.medium.com/pitfalls-of-observing-flows-in-launchwhenresumed-2ed9ffa8e26a
❝ for a concise demonstration of this issue, see https://gist.github.com/fergusonm/88a728eb543c7f6727a7cc473671befc ❞
Therefore, unfortunately, we can't use the extension function of launchWhenX to help control what life cycle state a flow is observed in. So what can we do? To take a step back, if we take a moment to see what we want to do, we can more easily find a solution and observe only in a specific lifecycle state. When we decompose this problem, we notice that what we really want to do is start observation in one state and stop observation in another state.
If we use another tool, such as RxJava, we can subscribe to the event flow in the onStart lifecycle callback and handle it in the onStop callback. (a similar pattern can also 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() }
Why can't we do this with Flow and coroutines? Well, we can. When the life cycle is broken, the scope will still be cancelled, but we can shorten the time when the observer is in the Activity state to 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() }
This satisfies the third requirement and solves the problem of observing event flow only in the state of security life cycle, but it introduces a large number of templates.
「Cleaning Things Up」
What if we delegate the responsibility for managing this work to something else to help eliminate these templates? Patrick Steiger's article "replacing StateFlow or SharedFlow with LiveData" has an amazing episode. (this is also a good reading). The original address is as follows: https://proandroiddev.com/should-we-choose-kotlins-stateflow-or-sharedflow-to-substitute-for-android-s-livedata-2d69f2bd6fa5
He created a set of extension functions to automatically subscribe to a traffic collector when the owner of a life cycle reaches the beginning, and cancel the collector when the life cycle reaches the stop stage. Here is my slightly modified version.
(October 2021 edit: see the update 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, {})
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) }
Now we have an event observer, which only observes after reaching the start life cycle, and cancels when reaching the stop life cycle.
It also has an additional advantage, that is, when the transition from stop to start of the life cycle is not common, but it is not impossible, it can restart Flow Collect.
This makes it safe to perform operations such as Fragment navigation or other lifecycle sensitive processing without worrying about the state of the lifecycle. Flow is collected only in the safe lifecycle state!
Pulling It All Together
Putting everything together is the basic pattern I use to define a "single live event" flow and how I can safely observe it.
To sum up: the event flow of the view model is defined as a channel receiving flow. This allows the view model to submit events without knowing the observer's state. Without an observer, the event is buffered.
The view (i.e. Fragment or Activity) observes the flow only after the lifecycle reaches the start state. When the lifecycle reaches the stopped event, the observation is cancelled. This allows the event to be handled safely without worrying about the difficulties caused by the Android lifecycle.
Finally, with the help of FlowObserver, the template is eliminated.
You can see the whole 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) } }
I would like to commend all the authors mentioned in this article. Their contribution to the community has greatly improved the quality of my work.
Errata
Edited in March 2021
It has been several months since I published this article. Google has provided a new tool (still in alpha status) and a solution similar to what I write below. You can read it here.
https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
Edited in October 2021
With Android X Lifecycle has been updated to version 2.4. You can now use the flowWithLifecycle or repeatWithLifecycle extension function instead of the one I defined above. for instance.
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)
You can also do the same manually with repeatWithLifecycle. There are a number of different ways to make it easier to read by extending functions. Here are my two favorite methods, but there are many changes.
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) }
There is also a usage example with any minimum Activity state.
viewModel.events .observeWithLifecycle(fragment = this, minActiveState = Lifecycle.State.RESUMED) { // do things } viewModel.events .observeWithLifecycle(lifecycleOwner = viewLifecycleOwner, minActiveState = Lifecycle.State.RESUMED) { // do things }
Original link: https://proandroiddev.com/android-singleliveevent-redux-with-kotlin-flow-b755c70bb055