Replace SingleLiveEvent with kotlin Channel / Flow

Photo by Mihai Moisa on Unsplash

After the announcement of the StateFlow implementation this year, I got curious about the possibility to totally replace LiveData. This means one less project dependency and achieve a more independent code from Android framework.

StateFlow is not the subject of this post but we can change the view states represented by a LiveData using a StateFlow. For SingleLiveEvent class, we need a different solution.

Following the MVVM pattern, ViewModel provides the view state and events/actions to the View. In the context of LiveData, the second could be implemented using the class SingleLiveEvent. Some examples of actions are: dialog show, snack bar display, screen navigation.

First approach: Channel

Channels provide a way to transfer a stream of values.

Change from val action = SingleLiveEvent<Action>()
to val action = Channel<Action>(Channel.BUFFERED)

and on the Activity side as simple as this:
viewModel.action.onEach{ ... }.launchIn(lifecycleScope)

Problem

Everything seemed to be working fine until I tested a configuration change that recreates my Activity. After that, the action is not executed anymore 😔

Bug after theme change

The Channel attached to the Activity lifecycle coroutine scope is canceled when Activity.onDestroy() is called as a side effect of coroutine context cancellation.

Lifecycle scope implementation detail

Solution

Instead of using Channel, I changed to BroadcastChannel + Flow. Similar but different. Let's see!😉

and observing it in the Activity:

BroadcastChannel is NOT a specialization of a Channel as the name would suggest. I even found Roman Elizarov comment about this:

Having thought about it a bit more, it looks the whole BroadcastChannel is a misnomer. They are not really channels! They are more like some kind of "hot flows".

Using the first approach with Channel, it implements SendChannel and ReceiveChannel that gets closed when the view lifecycle scope is cancelled.

On the other hand, BroadcastChannel only implements SendChannel. A new ReceiveChannel is created to collect items from the BroadcastChanel (openSubscription) every time we launch the Flow (from .asFlow). This way, only the ReceiveChannel is closed when the scope is cancelled and the BroadcastChannel remains opened.

Every flow collector will trigger a new broadcast channel subscription.
fun <T> BroadcastChannel<T>.asFlow()

LiveData only emits when the LifecycleOwner is on active state (State.STARTED).

If we use launch on our solution, we may have the problematic scenario:

  • App in background
    Our screen has gone to saved state but our action continues to be consumed. It may lead to an exception if we are trying to commit a fragment transaction for example.

Using launchWhenStarted we achieve the same LiveData behaviour that pauses its consumption if the lifecycle state is "lower" than Started.

Conclusion

I've written about a single LiveData use case exploring some common scenarios in which it may fail, improving our solution. LiveData is really useful and easy to work with Android, but we always need to consider and learn from other solutions.

Android Developer @iFood

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store