A safer way to collect flows from Android UIs

Manuel Vivo
Mar 24 · 7 min read

In an Android app, Kotlin flows are typically collected from the UI layer to display data updates on the screen. However, you want to collect these flows making sure you’re not doing more work than necessary, wasting resources (both CPU and memory) or leaking data when the view goes to the background.

In this article, you’ll learn how the Lifecycle.repeatOnLifecycle, and Flow.flowWithLifecycle APIs protect you from wasting resources and why they’re a good default to use for flow collection in the UI layer.

Wasting resources

A cold flow backed by a channel or using operators with buffers such as buffer, conflate, flowOn, or shareIn is not safe to collect with some of the existing APIs such as CoroutineScope.launch, Flow<T>.launchIn, or LifecycleCoroutineScope.launchWhenX, unless you manually cancel the Job that started the coroutine when the activity goes to the background. These APIs will keep the underlying flow producer active while emitting items into the buffer in the background, and thus wasting resources.

Note: A cold flow is a type of flow that executes the producer block of code on-demand when a new subscriber collects.

For example, consider this flow that emits Location updates using callbackFlow:

Note: Internally, callbackFlow uses a channel, which is conceptually very similar to a blocking queue, and has a default capacity of 64 elements.

Collecting this flow from the UI layer using any of the aforementioned APIs keeps the flow emitting locations even if the view is not displaying them in the UI! See the example below:

lifecycleScope.launchWhenStarted suspends the execution of the coroutine. New locations are not processed, but the callbackFlow producer keeps sending locations nonetheless. Using the lifecycleScope.launch or launchIn APIs are even more dangerous as the view keeps consuming locations even if it’s in the background! Which could potentially make your app crash.

To solve this issue with these APIs, you’d need to manually cancel collection when the view goes to the background to cancel the callbackFlow and avoid the location provider emitting items and wasting resources. For example, you could do something like the following:

That’s a good solution, but that’s boilerplate, friends! And if there’s a universal truth about Android developers, it’s that we absolutely detest writing boilerplate code. One of the biggest benefits of not having to write boilerplate code is that with less code, there are fewer chances of making a mistake!

Lifecycle.repeatOnLifecycle

Without further ado, the API you should use is Lifecycle.repeatOnLifecycle available in the lifecycle-runtime-ktx library.

Note: These APIs are available in the lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 library or later.

Take a look at the following code:

repeatOnLifecycle is a suspend function that takes a Lifecycle.State as a parameter which is used to automatically create and launch a new coroutine with the block passed to it when the lifecycle reaches that state, and cancel the ongoing coroutine that’s executing the block when the lifecycle falls below the state.

This avoids any boilerplate code since the associated code to cancel the coroutine when it’s no longer needed is automatically done by repeatOnLifecycle. As you could guess, it’s recommended to call this API in the activity’s onCreate or fragment’s onViewCreated methods to avoid unexpected behaviors. See the example below using fragments:

Important: Fragments should always use the viewLifecycleOwner to trigger UI updates. However, that’s not the case for DialogFragments which might not have a View sometimes. For DialogFragments, you can use the lifecycleOwner.

Note: These APIs are available in the lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 library or later.

Under the hood!

Visual diagram

repeatOnLifecycle prevents you from wasting resources and app crashes because it stops and restarts the flow collection when the lifecycle moves in and out of the target state.

Difference between using and not using the repeatOnLifecycle API

Flow.flowWithLifecycle

Note: This API name takes the Flow.flowOn(CoroutineContext) operator as a precedent since Flow.flowWithLifecycle changes the CoroutineContext used to collect the upstream flow while leaving the downstream unaffected. Also, similar to flowOn, Flow.flowWithLifecycle adds a buffer in case the consumer doesn’t keep up with the producer. This is due to the fact that its implementation uses a callbackFlow.

Configuring the underlying producer

The MutableStateFlow and MutableSharedFlow APIs expose a subscriptionCount field that you can use to stop the underlying producer when subscriptionCount is zero. By default, they will keep the producer active as long as the object that holds the flow instance is in memory. There are some valid use cases for this though, for example, a UiState exposed from the ViewModel to the UI using StateFlow. That’s ok! This use case demands the ViewModel to always provide the latest UI state to the View.

Similarly, the Flow.stateIn and Flow.shareIn operators can be configured with the sharing started policy for this. WhileSubscribed() will stop the underlying producer when there are no active observers! On the contrary, Eagerly or Lazily will keep the underlying producer active as long as the CoroutineScope they use is active.

Note: The APIs shown in this article are a good default to collect flows from the UI and should be used regardless of the flow implementation detail. These APIs do what they need to do: stop collecting if the UI isn’t visible on screen. It’s up to the flow implementation if it should be always active or not.

Safe Flow collection in Jetpack Compose

When collecting flows in Compose, use the Flow.flowWithLifecycle operator as follows:

Notice that you need to remember the flow that is aware of the lifecycle with locationFlow and lifecycleOwner as keys to always use the same flow unless one of the keys change.

In Compose, side effects must be performed in a controlled environment. For that, use LaunchedEffect to create a coroutine that follows the composable’s lifecycle. In its block, you could call the suspend Lifecycle.repeatOnLifecycle if you need it to re-launch a block of code when the host lifecycle is in a certain State.

Comparison with LiveData

Collecting flows using these APIs is a natural replacement for LiveData in Kotlin-only apps. If you use these APIs for flow collection, LiveData doesn’t offer any benefits over coroutines and flow. Even more, flows are more flexible since they can be collected from any Dispatcher and they can be powered with all its operators. As opposed to LiveData, which has limited operators available and whose values are always observed from the UI thread.

StateFlow support in data binding

Use the Lifecycle.repeatOnLifecycle or Flow.flowWithLifecycle APIs to safely collect flows from the UI layer in Android.

Android Developers

The official Android Developers publication on Medium