What are Coroutines?
Think of Coroutines as lightweight threads that are used to perform tasks asynchronously, a.k.a. non-blocking programming.
They are natively supported by many programming languages, such as Go, Python, Perl, Ruby, Kotlin, etc.
A Kotlin Coroutine is a feature in Kotlin that lets you write non-blocking, asynchronous code that doesn’t require context-switching.
Who this tutorial is for?
Although Coroutines are used in general-purpose programming quite often, this article will primarily focus on Coroutines in an Android context.
This article will be a guide on how to implement Coroutines and the know-hows on integrating them into your existing Android app. We’ll be taking a look at how to create Coroutines, how they work, and advanced usages to fine-tune it in an Android app.
The prerequisites are as followed:
- Knowledge of basic Kotlin syntax;
- Basic knowledge of RxJava2 (Recommended, not mandatory);
- Basic asynchronous programming experience in Android development;
- Android Studio 3.2 or above; and,
- Kotlin 1.3.0 or above.
Your app on the main thread
If you recently got a new phone, chances are it has a refresh rate of at least 60Hz. This means that your app has 16ms to perform tasks on the Android main thread. Keep in mind that there are phones with higher refresh rates, and this time period will vary.
These tasks are usually performed on the main thread of an Android app:
- XML parsing
- View inflation
- View measurement
- View positioning
So, as you can see, your app does quite a lot of processing on the main thread, and that’s where the need to perform your tasks on an asynchronous thread arises.
So why Kotlin Coroutines?
Kotlin Coroutines enhance asynchronous programming by being lightweight and essentially faster than a thread as they are stackless. What this means from a multiprocessing perspective, is that Kotlin Coroutines don’t map on the native CPU thread, hence there’s no context-switching on the processor.
Additionally as most phones have at least 4 cores these days, it might be a good idea to put all 4 cores to work!
Adding Kotlin Coroutines to your project
What’s noteworthy when it comes to adding Kotlin Coroutines to your project is that they have been stable since the release of Kotlin 1.3.0. So if you’ve been using any version of Kotlin that’s below 1.3.0, it’s recommended that you upgrade the version in Android Studio IDE. Note: At the time of writing this article, the latest version of Kotlin was 1.3.21.
Here’s how you’d include the latest version of Kotlin in your project-level build.gradle
file:
buildscript {
ext.kotlin_version = '1.3.21'
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // Kotlin Gradle Plugin
}
}
Once you’re done with that, add the following dependencies to your app-level build.gradle
file:
dependencies {
// Core dependency
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
// Android UI Programming
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
// RxJava2 & Reactive Programming
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.1.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.1.1'
}
If you’re using ProGuard, you might also want to add the following rules to your proguard-rules.pro
file:
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembernames class kotlinx.** {
volatile <fields>;
}
Prelude to launching your first Coroutine
Before we get into launching Coroutines, it’s very important to discuss the two different types of functions in the context of synchronous-asynchronous programming: blocking functions and suspending functions.
Blocking Functions
Simply put, blocking functions are synchronous. If two or more functions execute one after the other, in the order they were invoked, they are said to be blocking functions. As none of them can be “paused” while the others are done executing, there’s less flexibility, in terms of execution, when it comes to blocking functions.
Let’s consider two blocking functions fun A()
and fun B()
. Here’s how they would be represented:
Suspending Functions
Suspending functions are functions that can be “paused”, or as the name itself indicates, “suspended”.
The above two functions fun A()
and fun B()
, if made suspending functions, would be represented like this:
Here, we've suspend the execution of fun A()
until fun B()
is done executing, and once it’s done executing, we resume fun A()
again.
To sum it up, a suspending function is a function whose execution can be started, paused and resumed again.
In Kotlin, a suspending function can only be invoked from another suspending function. A suspending function can’t be called from a regular (or blocking) function.
To declare a suspending function in Kotlin, just add the suspend
modifier to your function. So,
fun A() {
// Do something here
// in your normal function.
}
becomes:
suspend fun A() {
// Do something here
// in your suspending function.
}
The anatomy of a Kotlin Coroutine
Before we get to using Coroutines in our app, it is very important to understand how Coroutines work under the hood and get a good understanding of the components that are responsible for launching and executing a Coroutine.
It’s important that we take a quick look at four Coroutine concepts here:
- CoroutineContext
- CoroutineScope
- Coroutine Builder
- CoroutineDispatcher
CoroutineContext
CoroutineContext
, as the name indicates, defines the context in which your Coroutine runs.
This is similar to the Context of Activity
or Fragment
, and is used to manage lifecycle-related operations, proper threading, debugging, and handling exceptions.
CoroutineScope
Your Coroutine needs CoroutineContext
to run, and this is provided by the interface, CoroutineScope
. This way you can restrict the scope of your Coroutine to the lifecycle of Activity
or Fragment
.
Coroutine Builder
Coroutine Builders are extension functions on your CoroutineScope
that let you build your Coroutine and manage its execution.
In Kotlin, we have a bunch of Coroutine Builders. The two most used Coroutine Buillders are launch
and async
. We will cover this in detail later in this article.
CoroutineDispatcher
CoroutineDispatcher
is an abstract class that is responsible for launching your Coroutines. There are 4 Dispatchers that a CoroutineDispatcher
can use to launch your Coroutine:
Dispatchers.Default
: As you can probably tell, this is the default Dispatcher. This launches your Coroutine by using a common pool of threads. Normally, the number of threads used here equal the number of cores present on the device the app is running on.Dispatchers.Main
: This is the Dispatcher which confines itself to the Main thread, and hence, only using one thread.Dispatchers.IO
: This provides a shared pool of 64 on-demand created threads to your Coroutine. This Dispatcher is used to execute long-running and blocking I/O operations.Dispatchers.Unconfined
: This lets your Coroutine run on the current thread, but if it is suspended and resumed, it will run on whichever thread that the suspending function is running on. Please note that an unconfined Dispatcher shouldn’t normally be used in code.
Apart from using these 4 Dispatchers, you can also:
- convert
Executor
s into Dispatchers by calling theasCoroutineDispatcher()
extension function on them; and - create private thread pools for your Coroutines with
newSingleThreadContext
andnewFixedThreadPoolContext
.
Launching your first Coroutine
The entire list of Coroutine Builders can be found here, but for brevity purposes, we shall only talk about launch
and async
Coroutine Builders.
launch
Using The launch
Coroutine Builder launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job
.
Here is the function definition for launch
, as described in the official documentation:
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job (source)
Here, the Job
class is used to represent a job of a Coroutine and is also used to manage the execution of said Coroutine. You can cancel a Coroutine’s execution if you cancel a Job
.
The diagram below depicts the lifecycle of a Job
:
- NEW: When the
Job
is first created. - ACTIVE: When the
Job
begins executing. - WAIT FOR CHILDREN: When the
Job
waits for its childrenJob
s to finish executing. - COMPLETED: When the
Job
and its childrenJob
s are done executing. - CANCELING: If the
Job
or one of its children fail, or are canceled. - CANCELED: When the
Job
is done being canceled.
Another noteworthy thing here is that the launch
Coroutine Builder is actually an extension function on CoroutineScope
. We will see how this ties into lifecycle aware components later.
Let’s dive into the three parameters of the launch
Coroutine Builder:
context: CoroutineContext
: This parameter is used to specify the context in which the Coroutine needs to run. By default, the value passed here isEmptyCoroutineContext
, which just provides an empty Coroutine context to your Coroutine.start: CoroutineScope
: This parameter is used to define the way you want to initiate your Coroutine. By default, this is provided an enum value ofCoroutineStart.DEFAULT
. Here are all the enum values forCoroutineStart
:- DEFAULT: This value begins to immediately execute the Coroutine.
- ATOMIC: This is similar to DEFAULT, except a Coroutine started in this mode cannot be cancelled before it begins execution.
- LAZY: This starts the Coroutine only when it is needed. This is commonly known as lazy initialization.
- UNDISPATCHED: This immediately starts the Coroutine but the Coroutine will suspend itself when it reaches a point of suspension in the current thread.
block: suspend CoroutineScope.() -> Unit
: This is the most important part of your Coroutine Builder. This is where the code that needs to be executed goes. It takes in a suspending block, which is an extension function onCoroutineScope
, and returns aUnit
.
Let us run an example of launch
:
class MyActivity : AppCompatActivity(), CoroutineScope {
// Job variable that is tied to the Activity lifecycle
lateinit var job: Job
// Overridden from CoroutineScope,
// Main context that is combined with the context of the Job as well
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// IO context that is combined with the context of the Job as well
val ioContext: CoroutineContext
get() = Dispatchers.IO + job
// Your Coroutine goes here
fun myCoroutine() = launch {
// This is a long-running task that runs with the coroutineContext
longRunningTask()
val longerJob: Job = launch(context = ioContext) {
// This is another long-running task that runs with the ioContext
anotherLongRunningTask()
}
}
// Method to wait for all Coroutines to finish executing
// Use the join() method to suspend the Coroutine until it’s done executing
fun waitForAllCoroutinesToFinish() = job.join()
override fun onCreate(...) {
super.onCreate(...)
// Create the Job variable in Activity’s onCreate()
// This job was earlier declared as lateinit and we’re initializing it here
job = Job()
}
override fun onDestroy() {
super.onDestroy()
// Clean up your Job variable by canceling it in the Activity’s onDestroy()
job.cancel()
}
}
In the above code, we’re running two Coroutines: one with the Dispatchers.Main
dispatcher, and one with the Dispatchers.IO
dispatcher. Both of these dispatchers tie into the Job
that we’re creating when the Activity
is first created. When the Activity
is destroyed, we are canceling our Job
, and hence, all Coroutines and their children Coroutines also be canceled.
Note: Make sure that you’re implementing CoroutineScope
in your Activity
and overriding coroutineContext
. This is needed for your Coroutine to work.
async
Using The async
Coroutine Builder is the same as launch
, with the exception that it returns a Deferred<T>
.
Here’s the method definition for async
:
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> (source)
This is the key difference between async
and launch
. Deferred<T>
returns a particular value of type T after your Coroutine finishes executing, whereas Job
doesn’t.
Think of it like this: launch
is more of a fire-and-forget Coroutine Builder, while async
actually returns a value after your Coroutine is done executing.
Note: A Deferred<T>
extends a Job
. So you can call cancel()
on it like you normally would to cancel your Coroutine. It doesn’t have the join()
method, but has the await()
method instead. This is a return type of T, i.e. it waits until your Coroutine is done executing and returns the resultant variable.
Since Deferred<T>
extends Job
, the lifecycle remains the same.
Let us run an example of async
:
class MyActivity : AppCompatActivity(), CoroutineScope {
// Job variable that is tied to the Activity lifecycle
lateinit var job: Job
// Overridden from CoroutineScope,
// Main context that is combined with the context of the Job as well
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// IO context that is combined with the context of the Job as well
val ioContext: CoroutineContext
get() = Dispatchers.IO + job
// Your Coroutine goes here
fun myCoroutine() = launch {
// This is a long-running task that runs with the coroutineContext
// This returns a Deferred value of type String
val deferred: Deferred<String> = async(context = ioContext) {
getResponseFromUrl()
}
// Waits for the Coroutine to finish and return the result
val resultString: String = deferred.await()
}
// Method to wait for all Coroutines to finish executing
// Use the join() method to suspend the Coroutine until it’s done executing
fun waitForAllCoroutinesToFinish() = job.join()
override fun onCreate(...) {
super.onCreate(...)
// Create the Job variable in Activity’s onCreate()
job = Job()
}
override fun onDestroy() {
super.onDestroy()
// Clean up your Job variable by canceling it in the Activity’s onDestroy()
job.cancel()
}
}
In the code above, we’ll get a String from a long-running network call via our async
Coroutine Builder. We’re using the ioContext
to do so and storing the Deferred<String>
value in deferred
. This way we can get the actual value once the Coroutine is done executing.
We'll call the deferred.await()
method immediate after. Since the await()
method here returns a String
, we’ll store it in our variable resultString
of type String
.
Where do I go from here?
We have explored Coroutines in detail and learned how to build them with the help of async
and launch
.
While these are basic usages of Kotlin Coroutines, we encourage you to explore this concept in depth with the following set of resources:
- Official Kotlin docs
- kotlinx.coroutines
- Using Kotlin Coroutines in your Android App - Google Codelab
- Kotlin Expertise
- Better Async with Kotlin Coroutines
- Android Suspenders
Additionally, here is a helpful cheatsheet that you might want to keep on your desk while writing Coroutines.
Conclusion
As we’ve seen, Kotlin Coroutines are an easy way to write asynchronous, non-blocking code. If used properly, we can reduce the boilerplate code that comes along with writing asynchronous code in Android.
Kotlin Coroutines are highly fine-tunable and a great solution to problems that commonly trouble Android developers when writing asynchronous code.
So go ahead, explore Coroutines in depth today and let them do all the heavy-lifting for your Android app!