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 Amanjeet Singh.
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
30 mins
- Why Use Coroutines?
- Getting Started
- Introduction to Coroutines
- Suspending vs. blocking
- Coroutine Terminology
- Adding Kotlin Coroutines support
- Setting Up Your Code
- Coroutine Jobs
- Using Dispatchers With Kotlin Coroutines
- Scoping Kotlin Coroutines
- Downloading Images With Kotlin Coroutines
- Applying the Snow Filter
- Putting Everything Together
- Resolving the Error
- Internal Workings of Coroutines
- Handling Exceptions
- Cleaning Up
- Writing Expressive Kotlin Coroutines
- Where to Go From Here?
Coroutine Terminology
Kotlin Coroutines give you an API to write your asynchronous code sequentially. Take this snippet of code for example:
val snowyBitmap = getFilteredBitmap()
showBitmap(bitmap)
Here, showBitmap()
uses the snowyBitmap
from getFilteredBitmap()
, which fetches the bitmap from a given API and applies a snow filter.
But this sequential code can also perform blocking operations, which would be better off in a different thread. You can do this by wrapping it with Kotlin Coroutines. But before that, let’s review some terminology for the Kotlin Coroutines API.
- Suspending functions: This kind of function can be suspended without blocking the current thread. Instead of returning a simple value, it also knows in which context the caller suspended it. Using this, it can resume appropriately, when ready.
-
CoroutineBuilders: These take a suspending lambda as an argument to create a coroutine. There are a bunch of coroutine builders provided by Kotlin Coroutines, including
async()
,launch()
,runBlocking
. -
CoroutineScope
: Helps to define the lifecycle of Kotlin Coroutines. It can be application-wide or bound to a component like the AndroidActivity
. You have to use a scope to start a coroutine. -
CoroutineDispatcher
: Defines thread pools to launch your Kotlin Coroutines in. This could be the background thread pool, main thread or even your custom thread pool. You’ll use this to switch between, and return results from, threads
All of these terms will sink in, once you start working with Kotlin Coroutines in the project.
Adding Kotlin Coroutines support
Now, let’s return to the example. You can solve the problem of blocking, by wrapping the getFilteredBitmap
function inside an async
block.
val snowyBitmap = async { getFilteredBitmap() }
// do some work
showBitmap(bitmap.await())
That marks showBitmap()
as a suspension point for the compiler, because it calls for the result of async()
. Now, as soon as the program needs a snowyBitmap
, it can await()
the result. This will try to fetch the filtered bitmap. async()
, runs immediately, wrapping the value in another construct. This construct is called Deferred
. Until you actually wait for the value, you don’t suspend or block the caller.
This means that if there’s any other code between the function call and its result, that code can execute freely. Also, by the time you call await()
if the value is computed, you can simply resume with the rest of the code. This is a really powerful feature of Kotlin Coroutines.
And it’s only a small piece of what Kotlin Coroutines have to offer, but to fully explore them, let’s switch to Snowy! :]
Setting Up Your Code
Before you can create coroutines, you have to add the dependencies to your Android project. Start by opening the app level build.gradle and adding the following dependencies:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
These dependencies are important for integrating the different components of coroutines in your project. They and also provide Android support of coroutines for your project, like the main thread dispatcher.
When you start working with Kotlin Coroutines, you’ll be hearing a lot about Jobs. It’s one of the basic constructs in the Kotlin Coroutines API. As such, it’s vital to understand what it does.
Coroutine Jobs
Open TutorialFragment.kt and a property named parentJob
of type Job as follows, under the companion object
:
private val parentJob = Job()
As the name suggests, a Job
represents a piece of work that needs to be done. Additionally, every Job
can be cancelled, finishing its execution. Because of that, it has a lifecycle and can also have nested children. Coroutine builders like launch()
and async()
return jobs as a result.
If you create a child for a Job
, by nesting coroutines, it forms a parent-child hierarchy. With the hierarchy, you can control all the children through this single instance of a Job
. If you cancel the parent, all of the children get canceled too. If a child fails in execution, the rest of the hierarchy fails, as well.
Describing a parent-child hierarchy helps you to control the lifecycle of coroutines from a single instance when using it inside of an Activity
or Fragment
.
This is why you’re declaring a parentJob
. You can use it to cancel and clear up all coroutines which you launched in TutorialFragment
.
Next, it’s important to understand how threading works with coroutines.
Using Dispatchers With Kotlin Coroutines
You can execute a coroutine using different CoroutineDispatchers
, as mentioned before. Some of the available CoroutineDispatchers
in the API are: Dispatchers.Main
, Dispatchers.IO
and Dispatchers.Default
.
You can use these dispatchers for the following use cases:
- Dispatchers.Default: CPU-intensive work, such as sorting large lists, doing complex calculations and similar. A shared pool of threads on the JVM backs it.
- Dispatchers.IO: networking or reading and writing from files. In short – any input and output, as the name states
-
Dispatchers.Main: recommended dispatcher for performing UI-related events. For example, showing lists in a
RecyclerView
, updatingViews
and so on.
You’ll use some of these dispatchers to switch between the main and background threads. One last step before you can launch coroutines – defining a CoroutineScope
.
Scoping Kotlin Coroutines
Now, to define the scope when the coroutine runs, you’ll use a custom CoroutineScope to handle the lifecycle of the coroutines.
To do it, declare a property, as shown below, and initialize it under the parentJob
:
private val coroutineScope = CoroutineScope(Dispatchers.Main + parentJob)
The plus()
operator helps you create a Set
of CoroutineContext
elements, which you associate with the coroutines in a particular scope. The contexts and their elements are a set of rules each Kotlin Coroutine has to adhere to.
This set of elements can have information about:
- Dispatchers, which dispatch coroutines in a particular thread pool and executor.
- CoroutineExceptionHandler, which let you handle thrown exceptions.
- Parent Job, which you can use to cancel all Kotlin Coroutines within the scope.
Both the CoroutineDispatcher
and a Job
implement CoroutineContext
. This allows you to sum them – using plus()
, to combine their functionality.
Now it’s finally time to launch some Kotlin Coroutines!
Downloading Images With Kotlin Coroutines
Now, you’ll write a coroutine to download an image from a URL. Add the following snippet at the bottom of TutorialFragment.kt:
// 1
private fun getOriginalBitmapAsync(tutorial: Tutorial): Deferred<Bitmap> =
// 2
coroutineScope.async(Dispatchers.IO) {
// 3
URL(tutorial.url).openStream().use {
return@async BitmapFactory.decodeStream(it)
}
}
Here’s what this code does:
- Creates a regular function,
getOriginalBitmapAsync()
, which returns aDeferred
Bitmap
value. This emphasizes that the result may not be immediately available. - Use the
async()
to create a coroutine in an input-output optimizedDispatcher
. This will offload work from the main thread, to avoid freezing the UI. - Opens a stream from the image’s
URL
and uses it to create aBitmap
, finally returning it.
Next, you have to apply the image filters to the result!