Kotlin Coroutines 101: Async Programming in Practice

Eury Pérez Beltré
ProAndroidDev
Published in
9 min readApr 15, 2024

--

Hello folks ✌🏼😎

As you start learning Kotlin or Android you will face the need to offload heavy tasks from the UI Thread. You won't like your users to complain about your app to be slow!

Embark on a journey from Kotlin coroutines novice to expert with this comprehensive article. We’ll guide you through the essential concepts, providing practical code samples every step of the way.

Table of Concepts

  1. Understanding Coroutines: A Primer
  2. Kotlin Coroutines Components
  3. Delving into CoroutineContext: Understanding the Core
  4. Unveiling the CoroutineScope: Your Gateway to Control
  5. Coroutine-builders: Creating Coroutines
  6. Launching Coroutines: Real-world Applications
  7. Wrapping up

Understanding Coroutines: A Primer

A coroutine is a programming technique that allows you to pause and resume the execution of a function at specific points without blocking the entire program.

Think of a coroutine as a helpful assistant that can handle multiple tasks at once, allowing your program to stay responsive and performant even during complex operations.

This is how you launch a new coroutine. Run it and see what it prints.

The suspend function

A suspend function in Kotlin is a function that can be paused and resumed at a later time without blocking the thread on which it’s called. It’s typically used within coroutines to perform asynchronous or long-running operations without blocking the program’s execution.

Suspend functions can only be called from other suspend function or from a coroutine. Let's analyze the use of the suspend function someHeavyTask().

You won't notice any difference from this to calling any other normal function. That's why according to the docs, suspend functions are sequential by default (docs).

💡 Note: runBlocking is a helper function, useful for testing purposes. It runs a new coroutine, wait for the completion of any inner work and block the current thread.

Kotlin Coroutines Components

The Kotlin coroutines library comprises several components, each serving a distinct purpose. Below, you’ll find a diagram illustrating the interconnections between these components:

In the next sections we are diving into these concepts. We'll cover it from bottom to top, connecting the dots between them as we show practical samples.

Delving into CoroutineContext: Understanding the Core

The word Context refers to the circumstances or conditions in which something exists or occurs.

A Kotlin coroutine context provides the execution context and various elements needed for a coroutine to run. It essentially defines the environment in which a coroutine operates, such as its threading behavior and other runtime characteristics.

The coroutine context can consist of one of more of these elements:

  • Dispatcher
  • Job
  • CoroutineExceptionHandler
  • CoroutineName

Dispatchers

Here's a definition from the documentation:

Dispatchers determines what thread or threads the corresponding coroutine uses for its execution. The coroutine dispatcher can confine coroutine execution to a specific thread, dispatch it to a thread pool, or let it run unconfined.

kotlin docs

Dispatchers is a Kotlin object with some properties, each one representing a different type of dispatcher:

  • Default: Uses a shared background pool of threads. This one is used if no other dispatcher is specified. It is recommended for CPU-intensive work.
  • Main: This dispatcher is confined to the main thread. Operates the UI.
  • IO: This one is designed for offloading blocking IO tasks to a shared pool of threads. Used for networking, files read/write, database access, etc.
  • Unconfined: It starts the coroutine in the caller thread, but only until the first suspension point. After suspension it resumes the coroutine in the thread that is fully determined by the suspending function that was invoked. The unconfined dispatcher is appropriate for coroutines which neither consume CPU time nor update any shared data (like UI) confined to a specific thread.

Here's how you can specify the dispatcher:

Job

As the name implies, this context element represents a background task or a job. Jobs are cancellable by calling the cancel() function on it. There are 2 type of Jobs: Job and SupervisorJob.

The difference between these is their cancellation policies:

  • Job: If one child coroutine fails, will make the parent fail, causing the sibling coroutines to fail.

If you run this, you'll notice that Completed 2nd coroutine is never printed out. That's because secondChild gets cancelled with firstChild's failure.

  • SupervisorJob: If one child coroutine fails, won't make the parent fail and the siblings can continue until they complete.

Now you will see Completed 2nd coroutine printed because the failure of the first one, didn't cancel the parent coroutine, cancelling the secondChild coroutine. You may need to open it in the Playground to see the full log.

CoroutineExceptionHandler

This context element will help us catch the exceptions, so we can show a proper error instead of letting the exception propagate. This won't change the cancellation behavior explained in the Job section.

To understand how it works, play with the code below. Switch between Job and SupervisorJob, run it and see the difference on what is printed:

💡 Note: You can combine multiple context elements with the + symbol.

CoroutineName

Assign a specific name to a coroutine can be helpful for debugging purposes. This element can help with this. Here's how you can use it:

If you run this code, you will see the name we specified represented:

Combining multiple Coroutine Context Elements

In all cases, the variable type will be CoroutineContext.

🧪 Experiment: Open this code in the playground and create your own combinations

Unveiling the CoroutineScope: Your Gateway to Control

A coroutine scope in Kotlin defines the lifecycle and boundaries within which coroutines can be launched and managed. It ensures structured concurrency by organizing coroutines and managing their lifecycles, allowing for controlled execution and cancellation of tasks.

The coroutine scope must be created on a component with a lifecycle, because it needs to be cancelled when it is no longer needed.

Building and using a CoroutineScope

Using the CoroutineScope() function

The simplest way to build a scope is using the CoroutineScope helper function that accepts a CoroutineContext as the argument:

Using the coroutineScope() suspend function

💡 Note that coroutineScope() is written with lowercase c

Sometimes we need to access a coroutineScope in a class with no lifecycle like a repository class. In that case you can use the coroutineScope() function:

Then you can use the getUserInfo() function:

The CoroutineScope and its context is inherited from the caller scope. In simple words repository.getUserInfo() is called from within a CoroutineScope, which. will be the same inside of getUserInfo().

Using the MainScope function

Another way to build a coroutine scope is using the MainScope function. If you take a look at the source code, it basically creates a new CoroutineScope with the SupervisorJob and the Main dispatcher as the context.

It is useful to easily build a coroutine scope suitable for UI operations.

Using the lifecycleScope and viewModelScope in Android

In Android, we have coroutine scope extensions and helper functions on some components like viewModels and the view (Activities, Fragments, Composables).

In viewModels you can easily access the viewModelScope. All coroutines launched by it will be cancelled when the viewModel is cleared.

When using the view-system we have the lifecycleScope available for us to use it when needed. It is cancelled when the view is destroyed:

In Compose, we have some side-effects. The first one is the composition-aware rememberCoroutineScope(). The coroutines launched with this scope will be cancelled once the composable leaves the composition:

Another way to launch a coroutine in compose is using the LaunchedEffect. It is cancelled when it leaves the composition. If any of its keys change it is cancelled and restarted:

Cancelling a CoroutineScope

All coroutines are launched by a CoroutineScope and are tied to it. If you cancel a CoroutineScope, all the coroutines launched by it, will be cancelled as well:

🧪 Experiment: Comment out cancel() and run it again.

Coroutine-builders: Creating Coroutines

Now that we know what is the coroutine context and how the coroutine scope works, it is time to discover some interesting use cases of launching coroutines.

Coroutine builders

There are multiple ways to create or launch a new coroutine. So far we have been using the launch {} builder. Let's dive a little more on coroutine builders.

The launch builder

The launch coroutine builder is also known as the fire-and-forget builder. Because it is used for operations that we don't need to get a value from. These are some sample usages:

  • Saving a value in the database
  • Writing to a file
  • Logging analytics
  • etc

In the previous examples you have seen how it is used. But let's analyze its arguments:

  • context: The coroutine context. By default it provides an empty coroutine context.
  • start: The CoroutineStart option. It provides CoroutineStart.Default if no other is provided. Default means eagerly start.
  • block: The lambda block representing the coroutine scope.

It returns a Job, which you can use to cancel the coroutine.

The async builder

If you need to get some value back from the coroutine execution and we need to perform 2 or more tasks in parallel, then we need to use the async coroutine builder. Its arguments are the same as the launch builder.

The operation above takes ~1s. Which is the longer time among all the async coroutines.

As you will notice await() is a suspend function and you may be wondering, how it is taking ~1s instead of ~1.5s since when the first await() is called it will suspend the execution until finished.

The reason is that as specified above, the default CoroutineStart value, immediately schedules coroutine for execution. So as soon as the async block is created, it will start running, instead of waiting for await() to be invoked.

If you want to change this, you can do so by sending a different CoroutineStart argument. You can confirm the execution time with the measureTime function:

🧪 Experiment: Remove the start argument, run it again and check the result.

WithContext

Above I specified that we need to use async only if there are 2 or more operations to be performed. If we need to invoke a suspend function in a specific CoroutineContext we can use withContext:

💡 launch and async are the most popular coroutine builders. But there are more, do some research and mention them in the comments.

Launching Coroutines: Real-world Applications

Now that we are already experts in the kotlin coroutine library 🤓, let's see some use cases:

I. Performing multiple heavy tasks sequentially

As we mentioned before, suspend functions are sequential by default. Also, we can call normal functions in our coroutine, which will execute sequentially:

II. Concurrent fire-and-forget operations

We learned about the launch coroutine builder and its purpose. This is how you can execute 2 heavy tasks in parallel when you don't need a return value:

II. Concurrent operations with a return value

We already talked about the async coroutine builder and its preferred use case. Here's another sample:

III. Performing operations concurrently after some other operation

This one is a mix of the previous two cases. As you may be thinking, we just need to call a function (suspend or not) and then launch multiple coroutines to handle the concurrency:

IV. Cancelling a Coroutine

Some times we need to cancel a coroutine under some circumstances. To do so, you just need the job and invoke cancel() on it.

V. Cancelling a CoroutineScope

As stated before, coroutine scopes can be cancelled. When it happens, all the coroutines launched by that coroutine scope will be cancelled as well:

VI Convert a callback based function a suspend function

When using coroutines we want to get rid of callback altogether. For this we can use the suspendCoroutine and suspendCancellableCoroutine helper functions for this:

The difference with suspendCancellableCoroutine is that it provides a CancellableContinuation. That allows to call resumeWithException. That will cancel the current coroutine with the specified exception.

Wrapping Up

Kotlin coroutines are a great way to perform background operations in a structured fashion and with an intuitive set of rules that makes it very customizable and will help you anticipate the result of your operations.

Also, it gets rid of the ugly callbacks that makes it very hard when it comes to execute operations concurrently and even sequentially.

If you want me to cover some specific use cases, just let me know in the comments section.

--

--

🔹 12+ years in Software Development | 9 years focused on Android Development | Google Developer Expert🔹