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ć

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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:

// 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()
}
private fun sampleMethod() {
  lifecycleScope.launch { 
    // Do some work here
  }
}

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

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:

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

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

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.

private fun runProcessingWithFlow() {
  flowCollectorJob = lifecycleScope.launch {
    FlowUtils.testDataFlow().collect {
      println("FLOW: value is: $it")
      adapter.addNumber(it)
    }
  }
}
I/System.out: FLOW: value is: -933318729
I/System.out: FLOW: DisneyActivity.onStop

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

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>
private fun runProcessingWithFlow() {
  lifecycleScope.launch {
    FlowUtils.testDataFlow()
      .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
      .collect {
      println("FLOW: value is: $it")
      adapter.addNumber(it)
    }
  }
}

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.

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now