Skip to main content

Command Palette

Search for a command to run...

A safer way to collect flows from Android UIs

Published
3 min read
A safer way to collect flows from Android UIs

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 It’s recommended to expose the Flow API from lower layers of your app hierarchy regardless of the flow producer implementation details. However, you should also collect them safely.

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.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.

// Implementation of a cold flow backed by a Channel that sends Location updates
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            try { offer(result.lastLocation) } catch(e: Exception) {}
        }
    }
    requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
        .addOnFailureListener { e ->
            close(e) // in case of exception, close the Flow
        }
    // clean up when Flow collection ends
    awaitClose {
        removeLocationUpdates(callback)
    }
}

Visual diagram

Circling back to the beginning, collecting locationFlow directly from a coroutine started with lifecycleScope.launch was dangerous since the collection keeps happening even when the View is in the background.

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.

1_fmQRBPMPpnO7NAO2bg0GKw.png

Comparison with LiveData You might’ve noticed that this API behaves similarly to LiveData, and that’s true! LiveData is aware of Lifecycle, and its restarting behavior makes it ideal for observing streams of data from the UI. And that’s also the case for the Lifecycle.repeatOnLifecycle, and Flow.flowWithLifecycle APIs!

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 On a different note, one of the reasons you might be using LiveData is because it’s supported by data binding. Well, so is StateFlow! For more information about StateFlow support in data binding, check out the official documentation.

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

176 views