Kotlin Coroutines in Android Reference Guide

In this post I’ll summarize Kotlin Coroutines framework for Android developers and share some advanced tips which will help you use Coroutines more efficiently.

Please note that this article covers a lot of ground, so it’s very dense. In this sense, it’s not exactly an educational resource. Therefore, if you already familiar with Coroutines, you can use this post as a quick reference, or to make sure that there are no gaps in your understanding. However, if you’re new to Coroutines, I suspect that this article might not be the right resource for you. Instead, I invite you to take a look at my comprehensive Coroutines course.

Coroutines Basics

To start a new coroutine, you need an instance of CoroutineScope first:

private val coroutineScope = CoroutineScope(Dispatchers.Main.immediate)

Dispatcher object passed into the scope will be used by all future coroutines that you’ll start in that scope.

Tip: while it’s not mandatory to specify a dispatcher, I recommend always specifying one for all top-level scopes in your app.

Once you have a reference to CoroutineScope object, you can start new coroutines “inside that scope” using its launch coroutine builder:

coroutineScope.launch {
    // coroutine body
}

You can also specify different dispatchers for individual coroutines:

coroutineScope.launch(Dispatchers.Default) {
    // coroutine body
}

Coroutines are concurrency constructs. Therefore, the code inside each coroutine executes concurrently with respect to any other code in your application:

val coroutineScope = CoroutineScope(Dispatchers.Default)

coroutineScope.launch {
    delay(10)
    repeat(10) {
        print((0..9).random())
    }
}

coroutineScope.launch {
    delay(10)
    repeat(10) {
        print(('a'..'z').random())
    }
}

println("two coroutines started")

// Prints "two coroutines started", followed by randomly mixed alphanumeric string:
// > two coroutines started
// > ivtkuxqg5776596745ap

When you start a new coroutine, you get back a reference to Job object. For all practical purposes, that reference represents the coroutine itself. Therefore, you can use coroutine’s job to perform various actions on the coroutine. For example, you can wait for coroutine’s completion:

val coroutineScope = CoroutineScope(Dispatchers.Default)

coroutineScope.launch {
    val job1 = coroutineScope.launch {
        delay(10)
        repeat(10) {
            print((0..9).random())
        }
    }

    val job2 = coroutineScope.launch {
        delay(10)
        repeat(10) {
            print(('a'..'z').random())
        }
    }

    joinAll(job1, job2)

    println("two coroutines started")
}

// Pprints randomly mixed alphanumeric string, followed by "two coroutines started":
// > slhkgy1ugcc094946120two coroutines started

Even though each started coroutine will use a specific Dispatcher object, you can transfer the execution to another dispatcher within the same coroutine:

val coroutineScope = CoroutineScope(Dispatchers.Main.immediate)

coroutineScope.launch {
    buttonStart.isEnabled = false // on UI thread
    withContext(Dispatchers.Default) {
        // on background thread
    }
    buttonStart.isEnabled = true // on UI thread
}

It’s important to remember that withContext function isn’t coroutine builder, so it won’t start a new coroutine. Therefore, the code inside this function will be executed sequentially with respect to the parent coroutine:

val coroutineScope = CoroutineScope(Dispatchers.Default)

coroutineScope.launch {
    println("first statement")
    withContext(Dispatchers.IO) {
        delay(10)
        println("second statement")
    }
    println("third statement")
}

// Prints:
// > first statement
// > second statement
// > third statement

Coroutine Scope and Context

From technical point of view, coroutine scope and coroutine context are effectively equivalent (CoroutineScope object simply wraps CoroutineContext object). However, they are used for different purposes. CoroutineScope starts new coroutines and controls their lifecycle. CoroutineContext is used to configure the enclosing scope and adjust coroutines behavior.

Since CoroutineContext is immutable, every time you change the context, you basically create a new one. In some cases the change is explicit, but it doesn’t have to be:

// explicit change of context
coroutineScope.launch(Dispatchers.Default) {
    ...
}

// implicit change of context (each started coroutine has a new Job in any case)
coroutineScope.launch {
    ...
}

You can replace multiple context elements at once using + operator:

coroutineScope.launch(Dispatchers.Default + CoroutineName("my coroutine")) {
    ...
}

An immediate corollary from the effective equivalence of scope and context is that whenever you change coroutine’s context, you create a new scope:

coroutineScope.launch {
    // new scope
    withContext(Dispatchers.IO) {
        // yet another new scope
    }
}

While I can’t see any profound practical implications from the above observation, I think it does assist in drawing the overall picture of Coroutines in your mind.

Nested Coroutines

In some situations, you’ll want to start coroutines from within other coroutines (e.g. parallel decomposition of algorithms). In these cases, you can use launch coroutine builder inside parent coroutines:

val coroutineScope = CoroutineScope(Dispatchers.Default)

// Compute the result and then save and log it concurrently
coroutineScope.launch {
    val result = computeSomething()

    launch(Dispatchers.IO) {
        saveResultToDatabase(result)
    }

    launch(Dispatchers.IO) {
        logResultToFile(result)
    }
}

If you need to exchange data between different coroutines, avoid using shared mutable state at all costs. Instead, in many cases, you can just use async coroutine builder to return results from coroutines:

            val coroutineScope = CoroutineScope(Dispatchers.Default)

            coroutineScope.launch {
                val deferred1 = async(Dispatchers.IO) {
                    return@async getUserInfoFromProviderX(user)
                }

                val deferred2 = async(Dispatchers.IO) {
                    return@async getUserInfoFromProviderY(user)
                }

                val results = awaitAll(deferred1, deferred2)
                
                if (results[0] == results[1]) {
                    // providers agree
                } else {
                    // providers disagree
                }
            }

It’s important to understand the difference between starting nested coroutines using outer coroutine’s scope, and any other scope. In the above two examples (where I used outer coroutine’s scope implicitly), I established “parent-child” relationship between coroutines. Therefore, outer and nested coroutines will operate according to Structured Concurrency paradigm:

val coroutineScope = CoroutineScope(Dispatchers.Default)

val outerJob = coroutineScope.launch {
    launch (Dispatchers.IO) {
        delay(10)
        println("short coroutine")
    }

    launch(Dispatchers.IO) {
        delay(20)
        println("long coroutine")
    }
}

outerJob.invokeOnCompletion {
    println("outer coroutine completed")
}

// Prints:
// > short coroutine
// > long coroutine
// > outer coroutine completed

However, if I use any other scope, then there will be no “parent-child” relationship and, as a consequence, no Structured Concurrency between outer and nested coroutines:

val coroutineScope = CoroutineScope(Dispatchers.Default)

val outerJob = coroutineScope.launch {
    coroutineScope.launch (Dispatchers.IO) {
        delay(10)
        println("short coroutine")
    }

    coroutineScope.launch(Dispatchers.IO) {
        delay(20)
        println("long coroutine")
    }
}

outerJob.invokeOnCompletion {
    println("outer coroutine completed")
}

// Prints:
// > outer coroutine completed
// > short coroutine
// > long coroutine

You can use the later approach to “decouple” nested coroutine from outer coroutine, but, in most cases, you wouldn’t want to “break” Structured Concurrency.

Tip: treat this approach as a code smell. It doesn’t mean that you should never use it, just that you need to be very suspicious when you see something like that in the code. State the reason for breaking Structured Concurrency in a comment for future maintainers.

Coroutines Cancellation

The simplest way to cancel a coroutine is to call cancel on its job:

val job = coroutineScope.launch(Dispatchers.IO) {
    delay(1000)
    println("inside coroutine")
}

job.cancel()

// Prints: nothing

However, keep in mind that coroutines cancellation is cooperative. Therefore, if the code inside a coroutine doesn’t account for potential cancellation, cancelling the coroutine will not prevent from that code to execute to completion:

val job = coroutineScope.launch(Dispatchers.Default) {
    for (i in 0 until 1000) {
        println(i)
    }
}

job.cancel()

// Prints: 0 to 999

You can make the code cooperative by either checking isActive flag, or “asserting” ensureActive:

val job = coroutineScope.launch(Dispatchers.Default) {
    for (i in 0 until 1000) {
        ensureActive()
        println(i)
    }
}

job.cancel()

// Prints numbers until the coroutine is cancelled

If you use isActive, always throw CancellationException when you find coroutine inactive. This isn’t strictly required in many cases, but it’s an act of defensive programming which will make your code more future-proof:

val job = coroutineScope.launch(Dispatchers.Default) {
    for (i in 0 until 1000) {
        if (!isActive) {
            println("coroutine cancelled")
            throw CancellationException("cancelled")
        }
        println(i)
    }
}

job.cancel()

// Prints numbers until the coroutine is cancelled, and then prints "coroutine cancelled"

Keep in mind that calling a general suspend function from a coroutine doesn’t guarantee cooperation on cancellation:

val job = coroutineScope.launch(Dispatchers.Default) {
    for (i in 0 until 1000) {
        printsuspend(i)
    }
}

job.cancel()

...

private suspend fun printsuspend(i: Int) {
    println(i)
}

// Prints: 0 to 999

Most (all?) suspending functions from Coroutines framework itself implement cancellation support internally. At the very least, they will check for cancellation before they start executing and right before they return:

val job = coroutineScope.launch(Dispatchers.Default) {
    for (i in 0 until 1000) {
        yield()
        println(i)
    }
}

job.cancel()

// Prints numbers until the coroutine is cancelled

Keep in mind that framework’s suspending functions throw CancellationException on cancellation. If you catch this exception and don’t re-throw it, cancellation will be effectively cancelled (sorry, I couldn’t find better words to describe this situation):

val job = coroutineScope.launch(Dispatchers.Default) {
    for (i in 0 until 1000) {
        try {
            delay(10)
        } catch (e: CancellationException) {
            println("coroutine cancelled")
        }
        println(i)
    }
}

job.cancel()

// Prints: 0 to 999, each time adding "coroutine cancelled"

Tip: treat catching CancellationException without re-throwing it subsequently as a code smell. Document special cases in comments for future maintainers.

The only valid reason to catch CancellationException that I can think of is to perform “cancellation cleanup”:

val job = coroutineScope.launch(Dispatchers.Default) {
    try {
        doSomething()
        doSomethingElse()
    } catch (e: CancellationException) {
        cleanUpOnCancellation()
        throw e
    }
    println("flow completed successfully")
}

job.cancel()

...

private fun cleanUpOnCancellation() {
    println("cleaning up")
}

// Prints: "cleaning up"

Keep in mind that if cleanUpOnCancellation is suspending function that supports cancellation, it won’t be executed. This is hell of a tricky bug:

val job = coroutineScope.launch(Dispatchers.Default) {
    try {
        doSomething()
        doSomethingElse()
    } catch (e: CancellationException) {
        cleanUpOnCancellation()
        throw e
    }
    println("flow completed successfully")
}

job.cancel()

...

private suspend fun cleanUpOnCancellation() = withContext(Dispatchers.IO) {
    println("cleaning up")
}

// Prints: nothing

To work around this problem, use NonCancellable job:

val job = coroutineScope.launch(Dispatchers.Default) {
    try {
        doSomething()
        doSomethingElse()
    } catch (e: CancellationException) {
        withContext(NonCancellable) {
            cleanUpOnCancellation()
            throw e
        }
    }
    println("flow completed successfully")
}

job.cancel()

... 

private suspend fun cleanUpOnCancellation() = withContext(Dispatchers.IO) {
    println("cleaning up")
}

// Prints: "cleaning up"

Tip: consider adopting this pattern into a code style guidelines for your project. In other words: make the bodies of each catch clause for CancellationException non-cancellable (regardless of whether they contain suspending calls, or not). This is yet another defensive programming practice, but it only makes sense if all developers follow it. Otherwise, it can lead to confusion.

By the way, the same applies to suspending calls within any finally clause you might have inside your coroutines:

val job = coroutineScope.launch(Dispatchers.Default) {
    try {
        doSomething()
        doSomethingElse()
    } finally {
        mandatoryCall()
    }
    println("flow completed successfully")
}

job.cancel()

...

private suspend fun mandatoryCall() = withContext(Dispatchers.IO) {
    println("very important logic")
}

// Prints: nothing

However, unlike catch clauses for CancellationException, you can’t just make all your finally clauses non-cancellable. Instead, you’ll need to carefully consider each of them and decide whether the logic there should be executed on cancellation or not.

When parent coroutine is cancelled, it cancels all its children:

val job = coroutineScope.launch(Dispatchers.Default) {
    launch {
        delay(25)
        println("child coroutine 1")
    }

    val deferred = async {
        delay(50)
        return@async "child coroutine 2"
    }

    println(deferred.await())
}

Thread.sleep(10)

job.cancel()

// Prints: nothing

Coroutine Scope Cancellation

In addition to cancelling individual coroutines, you can also cancel entire CoroutineScope objects. Cancelled scope cancels all its child coroutines:

val coroutineScope = CoroutineScope(Dispatchers.Default)

coroutineScope.launch {
    launch {
        delay(25)
        println("child coroutine 1")
    }

    val deferred = async {
        delay(50)
        return@async "child coroutine 2"
    }

    println(deferred.await())
}

Thread.sleep(10)

coroutineScope.cancel()

// Prints: nothing

Once a scope is cancelled, you can’t start new coroutines in it. If you try to, the coroutines will be cancelled before they even get a chance to execute:

val coroutineScope = CoroutineScope(Dispatchers.Default)

coroutineScope.launch {
    println("coroutine 1")
}

Thread.sleep(10)

coroutineScope.cancel()

coroutineScope.launch {
    println("coroutine 2")
}

// Prints "coroutine 1"

When you try to start a new coroutine in a cancelled scope, there is no crash, error, warning or any other indication of the problem. Your code will just silently fail. In my opinion, this is a big problem for long-term maintainability. Therefore, instead of cancelling the entire scope, you can just cancel its children:

val coroutineScope = CoroutineScope(Dispatchers.Default)

coroutineScope.launch {
    println("coroutine 1")
}

Thread.sleep(10)

coroutineScope.coroutineContext.cancelChildren()

coroutineScope.launch {
    println("coroutine 2")
}

// prints:
// > coroutine 1
// > coroutine 2

I thought a lot about scope cancellation, but couldn’t find one single reason to use this feature in your code. If the correctness of your logic is predicated on scope cancellation mechanics, that’s a huge red flag in my opinion.

Tip: I recommend avoiding cancellation of entire coroutine scopes. Instead, just cancel scopes’ children whenever needed.

Uncaught Exceptions in Coroutines

Uncaught exceptions thrown from within coroutines are treated as failures (except for CancellationException discussed earlier).

Whenever a coroutine fails, it’s cancelled immediately and, in addition, cancels its parent coroutine or scope. Since cancelled scope cancels all its children, failure in any coroutine leads to “global cancellation of everything” within the sub-tree of the cancelled scope:

val coroutineExceptionHandler = CoroutineExceptionHandler {
    coroutineContext, throwable -> // no-op
}

val coroutineScope = CoroutineScope(Dispatchers.Default + coroutineExceptionHandler)

var childJob1: Job? = null
var childJob2: Job? = null
val parentJob = coroutineScope.launch {
    childJob1 = launch {
        delay(25)
        throw RuntimeException("child coroutine 1 failed")
    }

    val deferred = async {
        delay(50)
        return@async "child coroutine 2"
    }

    childJob2 = deferred // Deferred extends Job

    println(deferred.await())

    println("parent coroutine")
}

Thread.sleep(100)

println("Scope Job: ${coroutineScope.coroutineContext[Job]}")
println("Parent Job: $parentJob")
println("Child 1 Job: $childJob1")
println("Child 2 Job: $childJob2")

// Prints:
// > Scope Job: JobImpl{Cancelled}@6b71769e
// > Parent Job: "coroutine#2":StandaloneCoroutine{Cancelled}@2752f6e2
// > Child 1 Job: "coroutine#3":StandaloneCoroutine{Cancelled}@e580929
// > Child 2 Job: "coroutine#4":DeferredCoroutine{Cancelled}@1cd072a9

In addition to cancelling the parent, uncaught exceptions from coroutines started with launch are also delegated to scope’s CoroutineExceptionHandler:

val coroutineExceptionHandler = CoroutineExceptionHandler {
    coroutineContext, throwable -> println("exception: $throwable")
}

val coroutineScope = CoroutineScope(Dispatchers.Default + coroutineExceptionHandler)

coroutineScope.launch {
    launch {
        throw RuntimeException("child coroutine 1 failed")
    }
    delay(10)
    println("parent coroutine")
}

// Prints:
// > exception: java.lang.RuntimeException: child coroutine 1 failed

If you don’t install custom exception handler into the scope, default one will be used. In Android apps, default CoroutineExceptionHandler delegates to apps’ default UncaughtExceptionHandler, which, in turn, crashes the app.

Unlike in coroutines started with launch, uncaught exceptions from coroutines started with async aren’t delegated to CoroutineExceptionHandler. Instead, they will be re-thrown when you call await on Deferred objects:

val coroutineExceptionHandler = CoroutineExceptionHandler {
    coroutineContext, throwable -> println("exception: $throwable")
}

val coroutineScope = CoroutineScope(Dispatchers.Default + coroutineExceptionHandler)

val deferred = coroutineScope.async {
    throw RuntimeException("child coroutine 1 failed")
}

delay(10)

try {
    deferred.await()
} catch (e: RuntimeException) {
    println("caught exception from await call: $e")
}

// Prints:
// > caught exception from await call: java.lang.RuntimeException: child coroutine 1 failed

However, there is a gotcha here! If async coroutine is a child of another coroutine, uncaught exception in it will be treated just like in child launch coroutine:

val coroutineExceptionHandler = CoroutineExceptionHandler {
    coroutineContext, throwable -> println("exception: $throwable")
}

val coroutineScope = CoroutineScope(Dispatchers.Default + coroutineExceptionHandler)

coroutineScope.launch {
    val childDeferred = async {
        throw RuntimeException("child coroutine 1 failed")
    }

    delay(10)

    try {
        childDeferred.await()
    } catch (e: RuntimeException) {
        println("caught exception from await call: $e")
    }
}

// Prints:
// > exception: java.lang.RuntimeException: child coroutine 1 failed

To prevent scope cancellation on children failures, you can add SupervisorJob object into its CoroutineContext:

val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    println("exception: $throwable")
}

val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + coroutineExceptionHandler)

var childJob1: Job? = null
var childJob2: Job? = null
val parentJob = coroutineScope.launch {
    childJob1 = launch {
        delay(25)
        throw RuntimeException("child coroutine 1 failed")
    }

    val deferred = async {
        delay(50)
        return@async "child coroutine 2"
    }

    childJob2 = deferred // Deferred extends Job

    println(deferred.await())

    println("parent coroutine")
}

launch {
    delay(50)
    println("standalone coroutine within scope")
}

Thread.sleep(100)

println("Scope Job: ${coroutineScope.coroutineContext[Job]}")
println("Parent Job: $parentJob")
println("Child 1 Job: $childJob1")
println("Child 2 Job: $childJob2")

// Prints:
// > exception: java.lang.RuntimeException: child coroutine 1 failed
// > Scope Job: SupervisorJobImpl{Active}@6b71769e
// > Parent Job: "coroutine#2":StandaloneCoroutine{Cancelled}@2752f6e2
// > Child 1 Job: "coroutine#4":StandaloneCoroutine{Cancelled}@e580929
// > Child 2 Job: "coroutine#5":DeferredCoroutine{Cancelled}@1cd072a9
// > standalone coroutine within scope

Note that in the above example SupervisorJob prevented scope’s cancellation, but the exception was still delegated to CoroutineExceptionHandler. That’s how it works.

Another gotcha! If you need to add supervision to individual coroutines, don’t add SupervisorJob to individual coroutines’ CoroutineContext:

val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    println("exception: $throwable")
}

val coroutineScope = CoroutineScope(Dispatchers.Default + coroutineExceptionHandler)

coroutineScope.launch {
    launch(SupervisorJob()) {
        throw RuntimeException("child coroutine 1 failed")
    }
    delay(10)
    println("parent coroutine")
}

// Prints:
// > exception: java.lang.RuntimeException: child coroutine 1 failed
// > parent coroutine

Based solely on the output from the above example, you might get an impression that everything works fine. After all, the exception was propagated to CoroutineExceptionHandler, but parent coroutine still executed to completion.

However, there is no proper parent-child relationship between the two coroutines, so no Structured Concurrency. This can lead to very tricky bugs. For example, “parent” coroutine won’t wait for the completion of “child” coroutine in this scenario (quotes because these aren’t proper parent-child coroutines).

Therefore, if you need supervision within individual coroutines, use supervisorScope function:

val coroutineScope = CoroutineScope(Dispatchers.Default + coroutineExceptionHandler)

coroutineScope.launch {
    launch() {
        supervisorScope {
            throw RuntimeException("child coroutine 1 failed")
        }
    }
    delay(10)
    println("parent coroutine")
}

// Prints:
// > exception: java.lang.RuntimeException: child coroutine 1 failed
// > parent coroutine

The output is the same, but now Structured Concurrency is preserved.

Tip (probably the most important piece of advice I can give in the context of uncaught exceptions handling): if your coroutines don’t throw exceptions, then you don’t need to deal with uncaught exceptions. Treat the approaches described in this section just as a “last line of defense” against unexpected exceptions and don’t couple your app’s logic to them.

Coroutine Dispatchers

Dispatchers.Main is analogous to Handler(Looper.getMainLooper()).post().

Dispatchers.Main.immediate is analogous to Activity.runOnUiThread().

If you never bothered with the distinction between UI Handler and Activity.runOnUiThread(), then you don’t need to bother with the distinction between teh above two “main” dispatchers either. Just choose one of them and use it consistently in the entire project. I recommend Dispatchers.Main.immediate.

I highly recommend avoiding Dispatchers.Default and Dispatchers.IO in Android apps. These two dispatchers represent very unfortunate dispatching strategy. Instead, I recommend using one single unbounded “background” dispatcher for all background work by default. You can see one potential implementation in the tutorial code for my Coroutines course.

If you’ll need special performance optimizations for specific features in your app, don’t go back to Dispatchers.Default. Instead, create dedicated dispatchers for each of these features. However, keep in mind that absolute majority of Android applications shouldn’t need this kind of optimizations, so take Knuth’s “preliminary optimization” warning seriously.

I know that many developers will be skeptical of my recommendation to avoid Dispatchers.Default and Dispatchers.IO in Android apps because it goes against the official recommendations. Well, this won’t be the first time the official guidelines are non-optimal. If you want to go down this rabbit hole, I also wrote an article explaining the problems with these dispatchers (the explanation is long).

I can hardly imagine why you’d need Dispatchers.Unconfined in Android applications, but will gladly read suggestions in the comments section.

Summary

That’s all.

I’ll keep this reference guide updated and will add additional info if I missed something important. Therefore, you can bookmark this page and come back later to get a refresher on Kotlin Coroutines framework.

If, while reading this article, you found some aspects that you’d like to learn in more depth, take a look at my comprehensive Coroutines course. It covers everything you read in this post, and much more.

As usual, thanks for reading and you can leave your comments below.

Check Out My Courses on Udemy

2 comments on "Kotlin Coroutines in Android Reference Guide"

Leave a Comment