Kotlin Coroutine Scope, Context, and Job made simple
Tags: Android App Development, AndroidDev, Programming, Mobile App Development, Kotlin

Thanks to Romain Guy for the magnificient photo!

When I first explore Coroutine about 3 years ago, we can just call launch directly to get a non-blocking coroutine started.

Now, this is not possible anymore. It needs a scope to start it, or using runBlocking . But what’s a scope?

The official document provides a good explanation, but it took me 3 days to slowly digest it. To understand Scope, one needs to understand the Coroutine Context.

After having a better grasp of it, let me try to simplify the learning based on my understanding below, and some additional findings I got beyond what’s in the documentation.

Basic revisit: what is Coroutine?

Before we move further, let’s recap on what is Coroutine.

Similar to Thread, that allow some task to operate asynchronously.

Unlike Thread, it can run on the same thread process and not block the thread (using suspend function). Pretty cool feature. Refers to the below doc for further explanation on how that’s possible.

Let’s experiment it

runBlocking {

println("runBlocking ${Thread.currentThread()}")

launch {

println("launch ${Thread.currentThread()}")

}

}

It will print

To understand the printing output of Thread, refer to this StackOverflow. Don’t worry about the main thread etc in the diagram. It will be explained later in the dispatcher section below.

Printing Coroutine ID on Android App

Note: If you print this on Android App using Log.d("Track", "....") , by default, it will not show Coroutine ID. You’ll get the below instead.

Track: runBlocking Thread[main,5,main]

Track: launch Thread[main,5,main]

To get the Coroutine ID printed, you’ll need to set

System.setProperty("kotlinx.coroutines.debug", "on" )

You can set it in the Application class

class MainApplication: Application() {

override fun onCreate() {

super.onCreate()

System.setProperty("kotlinx.coroutines.debug", "on" )

}

}

Then you’ll get

What is Job?

To better manage a Coroutine, a job is provided when we launch (or async etc). A job is part of the context of the Coroutine.

runBlocking {

val job = launch {

try {

println("launch job")

delay(1000)

println("finish job")

} catch (e: CancellationException) {

println("cancel job")

}

} yield() // to start the launch

job.cancel()

}

In the above example shows with the job , one can easily cancel the coroutine.

launch job

cancel job

Jobs can be hierarchical (parent-child relationship)

If a launch is triggered in another coroutine (under the same scope context), the job of the launch will be made the child job of the coroutine.

Such a parent-child relationship will trigger the 2 behavior below

1. When parent job cancel, children’s jobs are canceled as well.

runBlocking {

val parentJob = launch {

try {

println("launch parent job")

val childJob = launch {

try {

println("launch child job")

delay(1000)

println("finish child job")

} catch (e: CancellationException) {

println("cancel child job")

}

}

delay(1000)

println("finish parent job")

} catch (e: CancellationException) {

println("cancel parent job")

}

} delay(500)

parentJob.cancel()

}

Notice when the parent’s job is canceled, the child job is canceled as shown in the output below.

launch parent job

launch child job

cancel child job

cancel parent job

However, if we cancel the child’s job , the parent’s job continues on.

val parentJob = launch {

try {

println("launch parent job")

val childJob = launch {

try {

println("launch child job")

delay(1000)

println("finish child job")

} catch (e: CancellationException) {

println("cancel child job")

}

}

delay(500)

childJob.cancel()

delay(500)

println("finish parent job")

} catch (e: CancellationException) {

println("cancel parent job")

}

}

The result as below

launch parent job

launch child job

cancel child job

finish parent job

2. When a child’s job error, the parent’s job is canceled as well.

runBlocking {

val parentJob = launch {

try {

println("launch parent job")

val childJob = launch {

try {

println("launch child job")

delay(500)

throw Exception()

} catch (e: CancellationException) {

println("cancel child job")

}

}

delay(1000)

println("finish parent job")

} catch (e: CancellationException) {

println("cancel parent job")

}

}

}

Notice when the child’s job is error out, the parent’s job is canceled as shown in the output below.

launch parent job

launch child job

cancel parent job

Similarly, when the parent’s job is error out, the children’s job are canceled as shown in the output below.

runBlocking {

val parentJob = launch {

try {

println("launch parent job")

val childJob = launch {

try {

println("launch child job")

delay(1000)

println("finish child job")

} catch (e: CancellationException) {

println("cancel child job")

}

}

delay(500)

throw Exception()

println("finish parent job")

} catch (e: CancellationException) {

println("cancel parent job")

}

}

}

The output as below