Kotlin Coroutines Tutorial for Android: Getting Started

In this Kotlin coroutines tutorial, you’ll learn how to write asynchronous code just as naturally as your normal, synchronous code. By Luka Kordić.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Returning a Single Value From a Coroutine

Of course, you can use the async builder to get a single value, but that’s not its intended purpose. Instead, you should use withContext. It’s a suspending function that takes in a CoroutineContext and a block of code to execute as its parameters. An example usage can look like this:

suspend fun getTestValue(): String = withContext(Dispatchers.Main) { 
  "Test" 
}

Because withContext is a suspending function, you need to mark getTestValue with suspend as well. The first parameter to withContext is Dispatchers.Main, which means this code will be executed on the main thread. The second parameter is a lambda function that simply returns the "Test" string.

withContext isn’t used only to return a value. You can also use it to switch the execution context of a coroutine. That’s why it accepts CoroutineContext as a parameter.

Coroutine Context

CoroutineContext is a collection of many elements, but you won’t go through all of them. You’ll focus on just a few in this tutorial. One important element you’ve already used is CoroutineDispatcher.

Coroutine Dispatchers

The name “dispatchers” hints at their purpose. They’re responsible for dispatching work to one a thread pool. You’ll use three dispatchers most often:

  • Default: Uses a predefined pool of background threads. Use this for computationally expensive coroutines that use CPU resources.
  • IO: Use this for offloading blocking IO operations to a pool of threads optimized for this kind of work.
  • Main: This dispatcher is confined to Android’s main thread. Use it when you need to interact with the UI from inside a coroutine.

Improving Snowy’s Performance

You’ll use your knowledge about dispatchers to improve the performance of your code by moving applySnowEffect‘s execution to another thread.

Replace the existing implementation of loadSnowFilter with the following:

private suspend fun loadSnowFilter(originalBitmap: Bitmap): Bitmap =
 withContext(Dispatchers.Default) {
   SnowFilter.applySnowEffect(originalBitmap)
}

applySnowEffect is a CPU-heavy operation because it goes through every pixel of an image and does certain operations on it. To move the heavy work from the main thread, you wrap the call with withContext(Dispatchers.Default). You’re using the Default dispatcher because it’s optimized for tasks that are intensive on the CPU.

Build and run the project now.

two images in snowy app

You won’t see any difference on the screen, but you can attach a debugger and put a breakpoint on applySnowEffect. When the execution stops, you’ll see something like this:

worker thread example with debugger

You can see in the marked area that the method is executing in a worker thread. This means that the main thread is free to do other work.

Great progress so far! Now, it’s time to learn how to cancel a running coroutine.

Canceling a Coroutine

Cancellation plays a big role in the Coroutines API. You always want to create coroutines in a way that allows you to cancel them when their work is no longer needed. This means you’ll mostly create coroutines in ViewModel classes or in the view layer. Both of them have well-defined lifecycles. That gives you the ability to cancel any work that’s no longer needed when those classes are destroyed. You can cancel multiple coroutines running in a scope by canceling the entire scope. You do this by calling scope.cancel(). In the next section, you’ll learn how to cancel a single coroutine.

Coroutine Job

A Job is one of the CoroutineContext elements that acts like a handle for a coroutine. Every coroutine you launch returns a form of a Job. launch builder returns Job, while async builder returns Deferred. Deferred is just a Job with a result. Thus, you can call cancel on it. You’ll use jobs to cancel the execution of a single coroutine. To run the following example, open it in the Kotlin Playground. It should look like this:

import kotlinx.coroutines.*

fun main() = runBlocking {
 //1
 val printingJob = launch {
  //2
  repeat(10) { number ->
   delay(200)
   println(number)
  }
 }
 //3
 delay(1000)
 //4
 printingJob.cancel()
 println("I canceled the printing job!")
}

This example does the following:

  1. Creates a new coroutine and stores its job to the printingJob value.
  2. Repeats the specified block of code 10 times.
  3. Delays the execution of the parent coroutine by one second.
  4. Cancels printingJob after one second.

When you run the example, you’ll see output like below:

0
1
2
3
I canceled the printing job!

Jobs aren’t used just for cancellation. They can also be used to form parent-child relationships. Look at the following example in the Kotlin Playground:

import kotlinx.coroutines.*

fun main() = runBlocking {
 //1
 val parentJob = launch {
  repeat(10) { number -> 
   delay(200)
   println("Parent coroutine $number")
  }
  //2
  launch {
   repeat(10) { number -> 
    println("Child coroutine $number")    
   }
  }
 }
 //3
 delay(1000)
 //4
 parentJob.cancel()
}

This example does the following:

  1. Creates a parent coroutine and stores its job in parentJob.
  2. Creates a child coroutine.
  3. Delays the execution of the root coroutine by one second.
  4. Cancels the parent coroutine.

The output should look like this:

Parent coroutine 0
Parent coroutine 1
Parent coroutine 2
Parent coroutine 3

You can see that the child coroutine never got to execute its work. That’s because when you cancel a parent coroutine, it cancels all of its children as well.

Now that you know how to cancel coroutines, there’s one more important topic to cover — error handling.

Error Handling in Coroutines

The approach to exception handling in coroutines is slightly different depending on the coroutine builder you use. The exception may get propagated automatically, or it may get deferred until the consumer consumes the result.

Look at how exceptions behave for the builders you used in your code and how to handle them:

  • launch: Exceptions are thrown as soon as they happen and are propagated up to the parent. Exceptions are treated as uncaught exceptions.
  • async: When async is used as a root coroutine builder, exceptions are only thrown when you call await.

Coroutine Exception Handler

CoroutineExceptionHandler is another CoroutineContext element that’s used to handle uncaught exceptions. This means that only exceptions that weren’t previously handled will end up in the handler. Generally, uncaught exceptions can result only from root coroutines created using launch builder.

Open TutorialFragment.kt, and replace // TODO: Insert coroutineExceptionHandler with the code below:

private val coroutineExceptionHandler: CoroutineExceptionHandler =
 CoroutineExceptionHandler { _, throwable ->
  showError("CoroutineExceptionHandler: ${throwable.message}")
  throwable.printStackTrace()
  println("Caught $throwable")
}

This code creates an instance of CoroutineExceptionHandler and handles the incoming exception. To install the handler, add this code directly below it:

private val tutorialLifecycleScope = lifecycleScope + coroutineExceptionHandler

This piece of code creates a new coroutine scope called tutorialLifecycleScope. It combines the predefined lifecycleScope with the newly created coroutineExceptionHandler.

Replace lifecycleScope with tutorialLifecycleScope in downloadSingleImage.

private fun downloadSingleImage(tutorial: Tutorial) {
  tutorialLifecycleScope.launch {
    val originalBitmap = getOriginalBitmap(tutorial)
    val snowFilterBitmap = loadSnowFilter(originalBitmap)
    loadImage(snowFilterBitmap)
  }
}

Before you try this, make sure you turn on airplane mode on your phone. That’s the easiest way to trigger an exception in the code. Build and run the app. You’ll see a screen with an error message and a reload button, like below:

screenshot with an error

CoroutineExceptionHandler should only be used as a global catch-all mechanism because you can’t recover from an exception in it. The coroutine that threw an exception has already finished at that point.