Chapters

Hide chapters

Kotlin Coroutines by Tutorials

Third Edition · Android 12 · Kotlin 1.6 · Android Studio Bumblebee

Section I: Introduction to Coroutines

Section 1: 9 chapters
Show chapters Hide chapters

15. Coroutines in the UI Layer
Written by Luka Kordić

In the previous chapter, you learned to execute some work on a background thread and pass the result to the Android main thread for rendering. In this chapter, you’ll focus on Kotlin Flow usage in an Android app, specifically on collecting flows from the UI layer. You’ll see a couple of ways to use Kotlin Flow and the safest way to do so.

Note: If you still haven’t read Chapter 11, “Beginning With Coroutine Flow”, and Chapter 12, “SharedFlow & StateFlow”, it would be an excellent idea to do so before proceeding. Those chapters explain Flow and its APIs in detail.

Getting Started

Open the starter project for this chapter in Android Studio and explore the code a bit. You can also run the project. You’ll see a familiar home screen:

You’ll use the same project from the previous chapter, but you’ll work on different files. The two files you want to focus on are:

  • FlowUtils.kt in common/utils and
  • UiLayerActivity.kt in ui/activity package.

FlowUtils.kt serves as a data source for this chapter. It contains only one method with the following implementation:

fun testDataFlow() = flow {
  while (true) {
    emit(Random.nextInt())
    println("FLOW: Emitting item")
    kotlinx.coroutines.delay(500)
  }
}.flowOn(Dispatchers.Default)

A few things are going on here. First, you create a new flow from the given suspendable block. Inside the block, you create an infinite loop, which will emit a random integer every 500 milliseconds. Last, you switch the execution context of this flow to Dispatchers.Default by using the flowOn operator.

UiLayerActivity.kt contains a basic setup for a RecyclerView component, in which you’re going to show the data coming in from the flow.

Introducing Lifecycle Scope

You learned in previous chapters that every coroutine must be launched in a coroutine scope. Android provides first-class support for coroutine scopes via Lifecycle-aware components. This means every Android component with its own lifecycle has a built-in scope your app can use. Two of the most commonly used built-in scopes are:

  • viewModelScope, available in androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 or higher
  • lifecycleScope, available in androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 or higher

You’ll learn more about viewModelScope in the last chapter. Right now, it’s time to see how Lifecycle Scope can help you manage coroutines in the UI layer.

Each Lifecycle object has a LifecycleScope defined. Any coroutine launched in this scope gets canceled when the lifecycle reaches the DESTROYED state. To access the object of type LifecycleScope in an Activity, you can use either lifecycle.coroutineScope or lifecycleOwner.lifecycleScope properties. Without lifecycleScope, you would need to manually create and cancel the scope for your UI components. Look at an example of doing things manually and then compare that with the usage of lifecycleScope.

// 1
private val mainScope by lazy { MainScope() }

// 2
private fun sampleMethod() {
  mainScope.launch {
    // Do some work here
  }
}

override fun onDestroy() {
// 3
  mainScope.cancel()
  super.onDestroy()
}

Here’s a short breakdown of the code snippet above:

  1. You create a new scope by using an object of type MainScope you save in mainScope.
  2. In sampleMethod, you launch a new coroutine in mainScope to do some work.
  3. You make sure the scope is canceled in onDestroy by calling mainScope.cancel().

By using lifecycleScope, you can reduce the boilerplate code needed for creating and canceling the scope. It becomes as simple as this:

private fun sampleMethod() {
  lifecycleScope.launch { 
    // Do some work here
  }
}

You achieved the same behavior as in the previous example simply by using lifecycleScope.launch.

Note: In Fragments, always use viewLifecycleOwner.lifecycleScope. That’s because the Fragment’s view lifecycle can be different from the lifecycle of the Fragment itself.

Collecting Flows in the UI

In Android apps, you typically collect flows from activities or fragments to render data updates on the screen. While doing so, keep in mind some caveats to avoid wasting resources or leaking data when the view goes to the background. Look at the following example. Open UiLayerActivity.kt and navigate to runProcessingWithFlow. Replace it with the code below:

private fun runProcessingWithFlow() {
    lifecycleScope.launch {
      FlowUtils.testDataFlow().collect {
        println("FLOW: value is: $it")
        adapter.addNumber(it)
      }
    }
  }

This code is relatively simple. All you do here is launch a new coroutine that collects the data coming from the testDataFlow function. When a number comes in, you pass it to the adapter, adding it to the list. Build and run the app, then click the Flow Examples button on the intro screen. The list populates with a new number every half second, like in the image below.

You might think: Okay, this works. I successfully collected data from the flow and displayed it to the user. There’s one big but here, though: This code will keep the flow active and collect it even when the view goes to the background. Open the Logcat window in your Android Studio and type FLOW in the search field. While on the list screen in your app, tap the home button to send the app to the background. Look at the Logcat now. You’ll notice the flow keeps producing items, and the view keeps collecting them, even after onStop.

I/System.out: FLOW: DisneyActivity.onStop
I/System.out: FLOW: Emitting item
I/System.out: FLOW: value is: 2102722372
I/System.out: FLOW: Emitting item
I/System.out: FLOW: value is: 440356489

Launching Coroutines on Lifecycle Events

The lifecycle-runtime-ktx library gives you an option to use launchWhenX APIs. You have three methods available to use:

  • launchWhenCreated: Launches and runs the given block when the lifecycle is in at least the created state.
  • launchWhenStarted: Launches and runs the given block when the lifecycle is in at least the started state.
  • launchWhenResumed: Launches and runs the given block when the lifecycle is in at least the resumed state.

You can fix the previous example by using one of these methods. Replace runProcessingWithFlow from the previous example with the following piece of code:

private fun runProcessingWithFlow() {
  lifecycleScope.launchWhenStarted {
    FlowUtils.testDataFlow().collect {
      println("FLOW: value is: $it")
      adapter.addNumber(it)
    }
  }
}

Build and run the app.

You can see the app works the same. It collects the numbers from the flow and populates the list. Try to open the Logcat window again and send the app to the background by tapping the home button. You should see logs like this:

I/System.out: FLOW: DisneyActivity.onStop
I/System.out: FLOW: Emitting item
I/System.out: FLOW: Emitting item
I/System.out: FLOW: Emitting item

Using launchWhenStarted, you fixed the problem of collecting items while in the background. But the flow remains active and emits items. This isn’t a desired behavior because it can waste CPU resources for something that might never appear on the screen. Luckily, there’s a solution for that as well.

Canceling the Coroutine Manually

To start, in UiLayerActivity.kt, find the line TODO: Declare flowCollectorJob here and replace it with the declaration of a Job variable: private var flowCollectorJob: Job? = null.

Next, replace the old runProcessingWithFlow implementation with this one:

private fun runProcessingWithFlow() {
  flowCollectorJob = lifecycleScope.launch {
    FlowUtils.testDataFlow().collect {
      println("FLOW: value is: $it")
      adapter.addNumber(it)
    }
  }
}

Use the previously declared flowCollectorJob variable to store a reference to the coroutine created for collecting the flow. Finally, in onStop method, replace the line //TODO: Cancel flowCollectorJob here with flowCollectorJob?.cancel(). Run the app again, send it to the background and watch the logs in your Logcat.

I/System.out: FLOW: value is: -933318729
I/System.out: FLOW: DisneyActivity.onStop

You’ll notice both the producer and the collector stopped after the app went to the background. This solution works exactly as you want. But this is boilerplate code you have to write every time. It’s time to see how to achieve the same behavior without boilerplate code.

Repeating Code on Lifecycle Events

The best solution for this problem also comes from the lifecycle-runtime-ktx library. It’s easy to use, requires zero boilerplate code and, most importantly, is safe. To see an example of this, replace the old runProcessingWithFlow implementation with the following:

private fun runProcessingWithFlow() {
  lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
      FlowUtils.testDataFlow().collect {
        println("FLOW: value is: $it")
        adapter.addNumber(it)
      }
    }
  }
}

First, you need to launch a new coroutine because repeatOnLifecycle is a suspend function. It also takes a Lifecycle.State as a parameter. This parameter automatically runs the code block in the coroutine when the lifecycle reaches the specified state. The coroutine will be canceled when the ON_STOP event happens and will restart its execution if the lifecycle receives the ON_START event again. You can remove flowCollectorJob?.cancel() from onStop because you don’t need it anymore.

Note: It’s recommended to call repeatOnX methods from an activity’s onCreate or fragments onViewCreated methods to avoid unexpected behavior.

Build and run the app again. You’ll see the result remains the same as in the previous example. The app prints nothing after I/System.out: FLOW: DisneyActivity.onStop, and you didn’t have to think about canceling the started coroutine yourself.

Flow With Lifecycle

There’s one other option you can use to solve the problem at hand: flowWithLifecycle. It’s a Kotlin Flow operator with the following definition:

public fun <T> Flow<T>.flowWithLifecycle(
  lifecycle: Lifecycle,
  minActiveState: Lifecycle.State = Lifecycle.State.STARTED
): Flow<T>

As you can see, it’s an extension function on Flow. It accepts Lifecycle and Lifecycle.State as parameters. This operator emits values from the upstream flow when lifecycle is at least in minActiveState. When the state falls below minActiveState, emissions will stop. This operator uses repeatOnLifecycle under the hood. You might ask the questions When should I use flowWithLifecycle and when should I use repeatOnLifecycle? A good recommendation is to use flowWithLifecycle when you need to collect only one flow and use repeatOnLifecycle when you have multiple flows.

Try the following operator on the example you used throughout this chapter. Replace the previous implementation of runProcessingWithFlow with this one:

private fun runProcessingWithFlow() {
  lifecycleScope.launch {
    FlowUtils.testDataFlow()
      .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
      .collect {
      println("FLOW: value is: $it")
      adapter.addNumber(it)
    }
  }
}

Build and run. The behavior remains the same as in the last two examples. The app updates the list with every new number, and both the emission and collection stop after the app has been sent to the background.

Key Points

  • To collect a flow, you need to launch a new coroutine.
  • When launching coroutines in activities, use lifecycleScope.
  • When launching coroutines in fragments, use viewLifecycleOwner.lifecycleScope.
  • When collecting Flows from activities and fragments, be careful not to leak data or waste CPU resources and memory.
  • Use repeatOnLifecycle APIs when you want to collect multiple flows safely.
  • Use flowWithLifecycle to safely collect a single flow.

Where to Go From Here?

In this chapter, you focused on collecting flows from the UI layer and canceling work when it’s no longer needed. However, you might run into cases where it can help to have a data producer working in the background. For example, you might want to have fresh data when you return to the screen. It’s your job to decide how you want to implement and collect the flows for a specific use case.

If you want to learn more about this topic, here are a few helpful links:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.