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ć.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Kotlin Coroutines Tutorial for Android: Getting Started
25 mins
- Why Use Kotlin Coroutines?
- Getting Started
- Adding Kotlin Coroutines Support
- Introduction to Kotlin Coroutines
- Suspending vs. Blocking
- Creating Your First Coroutine
- Coroutine Builders
- Coroutine Scope
- GlobalScope
- Downloading Images in Parallel With async
- Returning a Single Value From a Coroutine
- Coroutine Context
- Coroutine Dispatchers
- Improving Snowy’s Performance
- Canceling a Coroutine
- Coroutine Job
- Error Handling in Coroutines
- Coroutine Exception Handler
- Try/Catch
- Where to Go From Here?
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.
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:
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:
- Creates a new coroutine and stores its job to the
printingJob
value. - Repeats the specified block of code 10 times.
- Delays the execution of the parent coroutine by one second.
- 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:
- Creates a parent coroutine and stores its job in
parentJob
. - Creates a child coroutine.
- Delays the execution of the root coroutine by one second.
- 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
: Whenasync
is used as a root coroutine builder, exceptions are only thrown when you callawait
.
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:
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.