aster.cloud aster.cloud
  • /
  • Platforms
    • Public Cloud
    • On-Premise
    • Hybrid Cloud
    • Data
  • Architecture
    • Design
    • Solutions
    • Enterprise
  • Engineering
    • Automation
    • Software Engineering
    • Project Management
    • DevOps
  • Programming
    • Learning
  • Tools
  • About
  • /
  • Platforms
    • Public Cloud
    • On-Premise
    • Hybrid Cloud
    • Data
  • Architecture
    • Design
    • Solutions
    • Enterprise
  • Engineering
    • Automation
    • Software Engineering
    • Project Management
    • DevOps
  • Programming
    • Learning
  • Tools
  • About
aster.cloud aster.cloud
  • /
  • Platforms
    • Public Cloud
    • On-Premise
    • Hybrid Cloud
    • Data
  • Architecture
    • Design
    • Solutions
    • Enterprise
  • Engineering
    • Automation
    • Software Engineering
    • Project Management
    • DevOps
  • Programming
    • Learning
  • Tools
  • About
  • Mobile
  • Programming

A Safer Way To Collect Flows From Android UIs

  • aster.cloud
  • March 25, 2021
  • 7 minute 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 LifecycleOwner.addRepeatingJob, 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.


Partner with aster.cloud
for your next big idea.
Let us know here.



From our partners:

CITI.IO :: Business. Institutions. Society. Global Political Economy.
CYBERPOGO.COM :: For the Arts, Sciences, and Technology.
DADAHACKS.COM :: Parenting For The Rest Of Us.
ZEDISTA.COM :: Entertainment. Sports. Culture. Escape.
TAKUMAKU.COM :: For The Hearth And Home.
ASTER.CLOUD :: From The Cloud And Beyond.
LIWAIWAI.COM :: Intelligence, Inside and Outside.
GLOBALCLOUDPLATFORMS.COM :: For The World's Computing Needs.
FIREGULAMAN.COM :: For The Fire In The Belly Of The Coder.
ASTERCASTER.COM :: Supra Astra. Beyond The Stars.
BARTDAY.COM :: Prosperity For Everyone.

Wasting resources

It’s recommended to expose the Flow<T> 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 <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/buffer.html" target="_blank" rel="noopener nofollow">buffer</a>, <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/conflate.html" target="_blank" rel="noopener nofollow">conflate</a>, <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-on.html" target="_blank" rel="noopener nofollow">flowOn</a>, or <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/share-in.html" target="_blank" rel="noopener nofollow">shareIn</a> is not safe to collect with some of the existing APIs such as <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html" target="_blank" rel="noopener nofollow">CoroutineScope.launch</a>, <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/launch-in.html" target="_blank" rel="noopener nofollow">Flow<T>.launchIn</a>, or <a class="ck ig" href="https://developer.android.com/reference/kotlin/androidx/lifecycle/LifecycleCoroutineScope" target="_blank" rel="noopener nofollow">LifecycleCoroutineScope.launchWhenX</a>, 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 <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/callback-flow.html" target="_blank" rel="noopener nofollow">callbackFlow</a>:

// 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)
    }
}

Note: Internally, <em class="el">callbackFlow</em> 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:

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

        // Collects from the flow when the View is at least STARTED and
        // SUSPENDS the collection when the lifecycle is STOPPED.
        // Collecting the flow cancels when the View is DESTROYED.
        lifecycleScope.launchWhenStarted {
            locationProvider.locationFlow().collect {
                // New location! Update the map
            } 
        }
        // Same issue with:
        // - lifecycleScope.launch { /* Collect from locationFlow() here */ }
        // - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope)
    }
}

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.

Read More  Faster Jetpack Compose View Interop With App Startup And Baseline Profile

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:

class LocationActivity : AppCompatActivity() {

    // Coroutine listening for Locations
    private var locationUpdatesJob: Job? = null

    override fun onStart() {
        super.onStart()
        locationUpdatesJob = lifecycleScope.launch {
            locationProvider.locationFlow().collect {
                // New location! Update the map
            } 
        }
    }

    override fun onStop() {
        // Stop collecting when the View goes to the background
        locationUpdatesJob?.cancel()
        super.onStop()
    }
}

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!

LifecycleOwner.addRepeatingJob

Now that we all are on the same page and know where the problem lies, it’s time to come up with a solution. The solution needs to be 1) simple, 2) friendly or easy to remember/understand, and more importantly 3) safe! It should work for all use cases regardless of the flow implementation details.

Without further ado, the API you should use is <strong class="hk jt">LifecycleOwner.addRepeatingJob</strong> available in the lifecycle-runtime-ktx library. Take a look at the following code:

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

        // Collects from the flow when the lifecycle is at least STARTED and
        // STOPS the collection when it's STOPPED.
        // It automatically restarts collecting when the lifecycle is STARTED again.
        lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
            locationProvider.locationFlow().collect {
                // New location! Update the map
            } 
        }
    }
}

addRepeatingJob takes a <a class="ck ig" href="https://developer.android.com/reference/android/arch/lifecycle/Lifecycle.State" target="_blank" rel="noopener nofollow">Lifecycle.State</a> as a parameter that 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 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 addRepeatingJob. As you could guess, it’s recommended to call these APIs in the activity’s onCreate or fragment’s onViewCreated methods to avoid unexpected behaviors. See the example below using fragments:

Read More  The Next Wave Of 5G To Arrive Early For Telstra Customers
class LocationFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // ...
        viewLifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
            locationProvider.locationFlow().collect {
                // New location! Update the map
            } 
        }
    }
}

Note: These APIs are available in the <em class="el">lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01</em> library or later.

Using repeatOnLifecycle under the hood

To provide more flexible APIs and preserve the calling CoroutineContext, the <strong class="hk jt">suspend Lifecycle.repeatOnLifecycle</strong> function is also available for you to use. repeatOnLifecycle suspends the calling coroutine, re-launches the block when the lifecycle moves in and out of the target state, and resumes the calling coroutine when the Lifecycle is destroyed.

This API is helpful whenever you need to do one time setup tasks that need to suspend before doing the repeating work. See example below:

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

        lifecycleScope.launch {
            // One time setup task
            val expensiveObject = createExpensiveObject()

            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Repeat when the lifecycle is STARTED, cancel when STOPPED
                // Do work with expensive object
            }

            // `lifecycle` is DESTROYED when the coroutine resumes. repeatOnLifecycle
            // suspends the execution of the coroutine until the lifecycle is DESTROYED.
        }
    }
}

Flow.flowWithLifecycle

You can also use the Flow.flowWithLifecycle operator when you have only one flow to collect. This API also uses the suspend Lifecycle.repeatOnLifecycle function under the hood, and emits items and cancels the underlying producer when the Lifecycle moves in and out of the target state.

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

        locationProvider.locationFlow()
            .flowWithLifecycle(this, Lifecycle.State.STARTED)
            .onEach {
                // New location! Update the map
            }
            .launchIn(lifecycleScope) 
    }
}

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

Even if you use these APIs, watch out for hot flows that could waste resources even if they aren’t collected by anyone! There are some valid use cases for them, but do keep that in mind and document it if needed. Having the underlying flow producer active in the background, even if wasting resources, can be beneficial for some use cases: you instantly have fresh data available rather than catching up and temporarily showing stale data. Depending on the use case, decide whether the producer needs to be always active or not.

Read More  Developer Tips And Guides: Common Policy Violations And How You Can Avoid Them

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 <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html" target="_blank" rel="noopener nofollow">Flow.stateIn</a> and <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/share-in.html" target="_blank" rel="noopener nofollow">Flow.shareIn</a> operators can be configured with the sharing started policy for this. <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-sharing-started/-while-subscribed.html" target="_blank" rel="noopener nofollow">WhileSubscribed()</a> will stop the underlying producer when there are no active observers! On the contrary, <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-sharing-started/-eagerly.html" rel="noopener nofollow">Eagerly</a> or <a class="ck ig" href="https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-sharing-started/-lazily.html" rel="noopener nofollow">Lazily</a> 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

The <a class="ck ig" href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#collectasstate" target="_blank" rel="noopener nofollow">Flow.collectAsState</a> function is used in Compose to collect flows from composables and represent the values as <a class="ck ig" href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/State" target="_blank" rel="noopener nofollow">State<T></a> to be able to update Compose UI. Even if Compose doesn’t recompose the UI when the host activity or fragment is in the background, the flow producer is still active and can waste resources. Compose can suffer from the same problem as the View system.

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

@Composable
fun LocationScreen(locationFlow: Flow<Flow>) {

    val lifecycleOwner = LocalLifecycleOwner.current
    val locationFlowLifecycleAware = remember(locationFlow, lifecycleOwner) {
        locationFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
    }

    val location by locationFlowLifecycleAware.collectAsState()

    // Current location, do something with it
}

Notice that you need to <a class="ck ig" href="https://developer.android.com/jetpack/compose/state" target="_blank" rel="noopener nofollow">remember</a> 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. Therefore, LifecycleOwner.addRepeatingJob is not safe to use. Instead, use <a class="ck ig" href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#launchedeffect_1" target="_blank" rel="noopener nofollow">LaunchedEffect</a> 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

You might’ve noticed that this API behaves similarly to <a class="ck ig" href="https://developer.android.com/topic/libraries/architecture/livedata" target="_blank" rel="noopener nofollow">LiveData</a>, 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 LifecycleOwner.addRepeatingJob, suspend 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 <a class="ck ig" href="https://developer.android.com/kotlin/flow/stateflow-and-sharedflow" target="_blank" rel="noopener nofollow">StateFlow</a>! For more information about StateFlow support in data binding, check out the official documentation.

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

By Manuel Vivo
Source Medium


For enquiries, product placements, sponsorships, and collaborations, connect with us at [email protected]. We'd love to hear from you!

Our humans need coffee too! Your support is highly appreciated, thank you!

aster.cloud

Related Topics
  • Android
  • LiveData
  • StateFlow
  • UI
You May Also Like
View Post
  • Architecture
  • Data
  • Engineering
  • People
  • Programming
  • Software Engineering
  • Technology
  • Work & Jobs

Predictions: Top 25 Careers Likely In High Demand In The Future

  • June 6, 2023
View Post
  • Programming
  • Software Engineering
  • Technology

Build a Python App to Alert You When Asteroids Are Close to Earth

  • May 22, 2023
View Post
  • Programming

Illuminating Interactions: Visual State In Jetpack Compose

  • May 20, 2023
View Post
  • Computing
  • Data
  • Programming
  • Software
  • Software Engineering

The Top 10 Data Interchange Or Data Exchange Format Used Today

  • May 11, 2023
View Post
  • Gears
  • Mobile
  • Technology

Apple Watch Pride Edition Celebrates The LGBTQ+ Community

  • May 10, 2023
View Post
  • Architecture
  • Programming
  • Public Cloud

From Receipts To Riches: Save Money W/ Google Cloud & Supermarket Bills – Part 1

  • May 8, 2023
View Post
  • Programming
  • Public Cloud

3 New Ways To Authorize Users To Your Private Workloads On Cloud Run

  • May 4, 2023
View Post
  • Programming
  • Public Cloud

Buffer HTTP Requests With Cloud Tasks

  • May 4, 2023

Stay Connected!
LATEST
  • college-of-cardinals-2025 1
    The Definitive Who’s Who of the 2025 Papal Conclave
    • May 7, 2025
  • conclave-poster-black-smoke 2
    The World Is Revalidating Itself
    • May 6, 2025
  • 3
    Conclave: How A New Pope Is Chosen
    • April 25, 2025
  • Getting things done makes her feel amazing 4
    Nurturing Minds in the Digital Revolution
    • April 25, 2025
  • 5
    AI is automating our jobs – but values need to change if we are to be liberated by it
    • April 17, 2025
  • 6
    Canonical Releases Ubuntu 25.04 Plucky Puffin
    • April 17, 2025
  • 7
    United States Army Enterprise Cloud Management Agency Expands its Oracle Defense Cloud Services
    • April 15, 2025
  • 8
    Tokyo Electron and IBM Renew Collaboration for Advanced Semiconductor Technology
    • April 2, 2025
  • 9
    IBM Accelerates Momentum in the as a Service Space with Growing Portfolio of Tools Simplifying Infrastructure Management
    • March 27, 2025
  • 10
    Tariffs, Trump, and Other Things That Start With T – They’re Not The Problem, It’s How We Use Them
    • March 25, 2025
about
Hello World!

We are aster.cloud. We’re created by programmers for programmers.

Our site aims to provide guides, programming tips, reviews, and interesting materials for tech people and those who want to learn in general.

We would like to hear from you.

If you have any feedback, enquiries, or sponsorship request, kindly reach out to us at:

[email protected]
Most Popular
  • 1
    IBM contributes key open-source projects to Linux Foundation to advance AI community participation
    • March 22, 2025
  • 2
    Co-op mode: New partners driving the future of gaming with AI
    • March 22, 2025
  • 3
    Mitsubishi Motors Canada Launches AI-Powered “Intelligent Companion” to Transform the 2025 Outlander Buying Experience
    • March 10, 2025
  • PiPiPi 4
    The Unexpected Pi-Fect Deals This March 14
    • March 13, 2025
  • Nintendo Switch Deals on Amazon 5
    10 Physical Nintendo Switch Game Deals on MAR10 Day!
    • March 9, 2025
  • /
  • Technology
  • Tools
  • About
  • Contact Us

Input your search keywords and press Enter.