Hero image

Asynchronous Programming in Kotlin using Coroutines

Dec 24, 2023
Kotlin

In this article, we’ll talk about coroutines in general and how to use them in Kotlin to write asynchronous code.

What is a coroutine?

The term coroutine is a portmanteau of cooperative and routine. A routine is simply a block of code or a function. The cooperative part comes from the idea that a routine can pause to allow another to execute. In essence, a coroutine is a block of code that can be paused and resumed allowing tasks to be executed concurrently in an efficient way.

So, why should I care about coroutines?

Often your applications will run multiple tasks concurrently and you want those tasks to pause when appropriate to allow another to execute in the meantime. For example, when rendering a frame and performing a network request you want the network request to pause while it’s waiting for a response to allow the frame to render then resume when the response is available.

But can't this be achieved with threads?

Yes, it can you could pass network calls off to a separate thread however threads are much more expensive than coroutines - a thread consumes ~1MB of memory compared to a coroutine which is essentially free. Even if you use thread pools to avoid some of the costs of threads many languages, such as Kotlin, provide features that make managing coroutines easier than working with threads.

Coroutines in Kotlin

Coroutines are a built-in feature of the Kotlin language and you can use the kotlin.coroutines package to work with them however this only provides very basic coroutine support. To use more advanced features you’ll want to import the kotlinx-coroutines-core dependency into your project.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core'

This library provides many functions and extensions for working with coroutines. For example, runBlocking which is a coroutine builder that bridges between non-coroutine and coroutine code i.e. it starts executing the code inside a provided lambda and blocks until it is complete. Note: we use delay() to simulate a period where a function is suspended e.g. while making a network request.

fun main(): Unit = runBlocking {
    println("Started task 1")
    delay(1000)
    println("Completed task 1")

    println("Started task 2")
    delay(500)
    println("Completed task 2")
}

Which outputs:

Started task 1
Completed task 1
Started task 2
Completed task 2

That doesn't seem too exciting...

Indeed, the code pretty much does exactly what you’d expect and this is because the code in a coroutine just runs sequentially. I suspect what you wanted to see was both tasks starting simultaneously followed by task 2 completing then task 1 completing. To do that we need to start separate inner coroutines that run concurrently for each of the tasks. The launch coroutine builder can be used to start a coroutine in a fire-and-forget fashion.

fun main(): Unit = runBlocking {
    launch {
        println("Started task 1")
        delay(1000)
        println("Completed task 1")
    }

    launch {
        println("Started task 2")
        delay(500)
        println("Completed task 2")
    }
}

This outputs:

Started task 1
Started task 2
Completed task 2
Completed task 1

That's more like it! What if I want to wait for those tasks to finish before doing something else?

launch returns an instance of Job which can be used to wait for the tasks to complete, we’ll talk more about this in another article.

There are some other handy coroutine builders notably:

  • async: perform some operation asynchronously and get a result e.g. a database fetch.
  • produce: send values to a channel asynchronous for consumption.

Suspending Functions

The suspend keyword in Kotlin is used to indicate that a function or a lambda expression can be suspended. If we want to move our task from above into a function we would need to mark it as suspendible like so:

suspend fun task() {
    println("Started task")
    delay(1000)
    println("Completed task")
}

A suspending function must be called within a coroutine block but is otherwise the same as any other function.

fun main() = runBlocking {
    task()
}