Blog Infos
Author
Published
Topics
Published
Topics
Photo by Antony Trivet (Unsplash)

 

Understanding coroutines — really understanding them, not just learning patterns — comes from seeing what goes on under the hood.

My last post pulled apart common coroutine patterns in Android to see why they work. The more we dug, the more we saw there were three concepts woven like threads through everything: context, scopes and Job.

So it seems like those might be the gateway to understanding what coroutines are — and therefore key to using them properly.

With that in mind, let’s have some fun pulling at those threads*…

(* pun slightly intended)

Demo 1: Firing and forgetting coroutines

A coroutine scope is a box into which you place coroutines. It exists to restrict them.

You can see the value of that when you want to cancel coroutines — just destroy the box, and all the coroutines inside it get cancelled. This is brilliant, because it means you don’t have to keep track of coroutines you create. Their lifecycle is managed by the “box”.

To give a concrete example, here’s a button which cycles its background colour when clicked:

https://miro.medium.com/v2/resize:fit:750/format:webp/1*Xb48gAvABkH5LBhRstBB_g.gif

@Composable
fun RandomColourButton() {
val scope = rememberCoroutineScope()
var buttonColor by remember { mutableStateOf(Color.Red) }
Column {
Button(colors = ButtonDefaults.buttonColors(backgroundColor = buttonColor),
onClick = {
scope.launch {
while(true) {
delay(500)
buttonColor = Color(Random.nextInt(0xFF), Random.nextInt(0xFF), Random.nextInt(0xFF), 0xFF)
}
}
}
) {
Text("Cycle Random Colours")
}
}
}
view raw MainScreen.kt hosted with ❤ by GitHub

We saw in my last post why it’s safe to have the while(true) there, which would in any other context be horrific. But we skipped over something even more exciting!

…Which is this: See how we simply launched a coroutine on line 9, and forgot about it? We can do so perfectly safely because we launched it into a scope, in this case one that’s obtained by rememberCoroutineScope(). That scope gets cancelled when the RandomColourButton is taken off the screen, and so our launched coroutine is also cancelled.

To any developer coming from an environment which only has threads, not coroutines, it’s hard to overstate what a massive leap forward that is. It would be unthinkable to launch a thread and forget about it. You have to meticulously account for them all, on pain of weird bugs and resource leaks.

Demo 2: Cancel a launch()ed coroutine

When you launch a coroutine in a scope, as we did with scope.launch {…} in the code above, you get a Job. This Job represents the running coroutine.

You can cancel the coroutine (without affecting the parent scope, or any other coroutines in it) using Job.cancel():

val job = scope.launch {
while(true) {
delay(500)
buttonColor = Color(Random.nextInt(0xFF), Random.nextInt(0xFF), Random.nextInt(0xFF), 0xFF)
}
}
scope.launch {
delay(2000)
job.cancel()
}
view raw MainActivity.kt hosted with ❤ by GitHub

…and with the code above, the flashing button stops flashing after 2 seconds. We have saved the first coroutine’s Job and we launch a second coroutine to cancel it after a delay.

Demo 3: Launching a coroutine inside a coroutine

A Job can also have children. An easy way to create a child Job is to simply use launch {…} within a coroutine.

When a Job is cancelled, all its children are cancelled too. Here’s an example of that using our flashing button.

This time we’ve added another coroutine which increments a count:

https://miro.medium.com/v2/resize:fit:750/format:webp/1*ESBEwgreP7EpxnqXm4cF9A.gif

The coroutine which counts up is launched as a child of the coroutine which picks the random colour:

@Composable
fun FlashingCountingButton() {
val scope = rememberCoroutineScope()
var buttonColor by remember { mutableStateOf(Color.Red) }
var count by remember { mutableStateOf(1) }
Column {
Button(colors = ButtonDefaults.buttonColors(backgroundColor = buttonColor),
onClick = {
val job = scope.launch {
// launch a separate coroutine, inside this one, to increase the count
launch {
while(true) {
delay(500)
count ++
}
}
//...and here we change the background colour
while(true) {
delay(500)
buttonColor = Color(Random.nextInt(0xFF), Random.nextInt(0xFF), Random.nextInt(0xFF), 0xFF)
}
}
// cancel the above coroutine after 5 seconds
scope.launch {
delay(5_000)
job.cancel()
}
}
) {
Text("Cycle Random Colours (count = $count)")
}
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Job Offers

Job Offers


    Senior Android Engineer

    Carly Solutions GmbH
    Munich
    • Full Time
    apply now

    Senior Android Developer

    SumUp
    Berlin
    • Full Time
    apply now

OUR VIDEO RECOMMENDATION

No results found.

Jobs

Notice how the count stops when the flashing stops. That’s because the coroutine launched on line 12 is a child of the coroutine launched on line 10. When the parent is cancelled after a delay, it cancels the child, too.

In the world of coroutines, the laws governing this parent-child relationship (those same laws which ensure you can’t lose track of coroutines), are called structured concurrency.

Demo 4: Launching a coroutine in a context, specifying a job

There are other ways to launch a coroutine into a Job. You can specify the Job in a call to launch or async:

launch(myJob) {
  ...
}

Obviously, if you do this it’s up to you to carefully keep track of myJob, cancelling it when needed, so that this coroutine’s lifecycle is properly bounded.

The reason this works is because the argument to launch is a CoroutineContext, of which the job is an element.

coroutine context is just a collection of metadata about a coroutine. This includes its Job, name and dispatcher. See it as luggage tags attached to the coroutine, which explain to the coroutines library how to run it.

Demo 5: Launching a coroutine onto a different thread pool

One of those “luggage tags” — that is, one item of metadata attached to a coroutine — is the dispatcher. The coroutines library uses this to determine which thread or thread pool to run the coroutine on.

So using the same mechanism as above, you could launch a coroutine on the IO dispatchers thread pool:

launch(Dispatchers.IO) {
  ...
}

This works by creating a coroutine context which sets the dispatcher to Dispatchers.IO.

Demo 6: Other coroutine launching options (and combinations)

Or, you can combine elements of a coroutine context using the plus operator. Here’s how to launch a coroutine called “boo” into a new Job on the IO dispatchers thread pool:

launch(Dispatchers.IO + Job() + CoroutineName("boo")) {
  ...
}

When you create a coroutine context in this way, any elements you don’t mention will be inherited from the parent’s context. So if you have launch(Job()) in a coroutine being run on Dispatchers.IO, the launched child will also run on Dispatchers.IO. This is because its context’s dispatcher will have been inherited from its parent.

Demo 7: Using a coroutine scope — and what happens when you do

So we’ve seen:

  • Demo 1: A coroutine scope is kind of a lifetime-restricting container we can put coroutines into
  • Demo 3: A coroutine Job is … kind of a lifetime-restricting container we can put coroutines into.

Hmm. So what’s the difference?

As it turns out, there is no difference. Because:

A coroutine scope is just a wrapper for a coroutine context, which holds a Job.

…Which you can see from the coroutines library code:

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

Why, then, have separate types and a whole different name for essentially the same thing? The great Roman Elizarov, former project lead for Kotlin language, gives the analogy of a ship which has many different names for “rope”. Why? Because they are named by usage. A halyard is a rope to pull up the main sail, a downhaul is one to pull it down, a sheet is used to trim a sail.

In the analogy, a coroutine scope is so named because its intended use is as a particular kind of coroutine context. It’s one used to limit and control coroutines’ lifespans.

Under the hood, that means a coroutine scope will have a Job attached to a specific lifecycle:

  • viewModelScope has a Job which is cancelled when the view model is cleared
  • rememberCoroutineScope() has a Job which is cancelled when the composable exits the composition
  • viewLifecycleOwner.lifecycleScope has a Job which is cancelled when the lifecycle of the Android View ends

…and so on.

That takes us all the way back to demo 1, in which we saw it was safe to fire and forget a coroutine as it’s placed inside a scope. We can now see that’s because the scope has a Job bound to a relevant lifecycle.

To summarize…

The concepts of coroutine context, scope and Job underpin a large part of we do with coroutines. We’ve seen how context is like a list of metadata associated with a coroutine which provides information to observers and tells the dispatcher how to run it. We’ve seen how a Job represents a running coroutine, and can act as a parent to other coroutines. And from that we can understand a coroutine scope, which is a context with a parent Job.

I hope this has been helpful. As always, post any questions below!

Tom Colvin has been architecting software for two decades and is particularly partial to working with Android. He’s co-founder of Apptaura, the mobile app specialists, and available on a consultancy basis.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
This is the second article in an article series that will discuss the dependency…
READ MORE
blog
Let’s suppose that for some reason we are interested in doing some tests with…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu