Kotlin Coroutine Essentials: Everything you need to know

Sagar MalhotraSagar Malhotra
6 min read

This is my second article in the Coroutines series, The following is the list of articles you can check to understand Coroutines thoroughly.

  1. Coroutines, What, How, and Why?

  2. Coroutine Essentials(this.article).

  3. Coroutines Internal working.

I expect you to know What coroutines are, and where you are using them in your application. If you are not familiar yet, read this.

First, we will start with building our coroutines using some well-known Coroutine builders:

Launch:

The launch coroutine builder is used to start a new coroutine that does not return a result. It is for the fire-and-forget tasks, where you have to just call some long-running functions and do not care about what they return.

fun main() {
    println("Start")

    // Launch a coroutine
    GlobalScope.launch {
        delay(1000) // Simulate some background work
        println("Coroutine completed")
    }

    println("End")
}

//Start
//End
//...
//Coroutine completed

Async:

The async coroutine builder is used to start a coroutine that returns a Deferred value. We can call the suspending function await on the deferred value to wait and get the result.

fun main() {
    println("Start")

    val deferredResult = GlobalScope.async {
        delay(1000) // Simulate some background work
        "Coroutine completed"
    }

    // Do some other work in the meantime

    // Retrieve the result from the deferred value
    runBlocking {
        val result = deferredResult.await()//await is a suspend function
        println(result)
    }

    println("End")
}
//Start
//Coroutine completed
//End

CoroutineScope:

Consider this as the Mother of Coroutines. A CoroutineScope keeps track of any coroutine you are creating, just like a Mother takes care of her Children.

The ongoing work (running coroutines) can be canceled by calling scope.cancel() at any point in time.

You should create a CoroutineScope whenever you want to start and control the lifecycle of coroutines in a particular layer of your app, like in Android, there are viewModelScope and lifecycleScopeor GlobalScope for the whole application lifecycle.

When creating CoroutineScope it takes a CoroutineContext as a parameter to its constructor. You can create a new scope & coroutine with the following code:

// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
    // new coroutine
}

Job:

A Job instance in the coroutineContext represents the coroutine itself. A Job is a handle to a coroutine. For every coroutine that you create (by launch or async), it returns a Job instance that uniquely identifies the coroutine and manages its lifecycle.

A Job can go through a set of states: New, Active, Completing, Completed, Cancelling, and Cancelled. While we don’t have access to the states themselves, we can access properties of a Job: isActive, isCancelled and isCompleted.

States of a Job/Coroutine:

Types of Jobs:

  1. Job: It represents a single coroutine and provides control over its lifecycle, such as starting, waiting, and canceling. It can be used for simple asynchronous tasks.

  2. DeferredJob: It represents a coroutine that produces a result of type T and provides a way to await the result using the await function. Deferred is used when you need to perform computations concurrently and asynchronously and obtain a result when it's ready.

  3. SupervisorJob: A job type that is used as a parent job for child coroutines. Unlike regular jobs, a failure or cancellation of a child's coroutine does not propagate to its parent and other siblings. It’s useful when you want to isolate failures in a specific branch of coroutines.

  4. CompletableJob: A type of job that can be completed explicitly using the complete() function. It's often used in custom implementations or when you want to create custom ways of handling the job's lifecycle.

CoroutineContext:

A CoroutineContext is an interface that represents a context of execution for coroutines. It provides a set of elements that define the behavior of a coroutine. This context is crucial for managing coroutine execution, including handling concurrency, thread pooling, and scheduling.

It’s made of:

A CoroutineContext for a Coroutine = Its new Job + Other things inherited from parent(If any).

Since a CoroutineScope can create coroutines and you can create more coroutines inside a coroutine, an implicit task hierarchy is created. In the following code snippet, apart from creating a new coroutine using the CoroutineScope, see how you can create more coroutines inside a coroutine:

val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
    // New coroutine that has CoroutineScope as a parent
    val result = async {
        // New coroutine that has the coroutine started by 
        // launch as a parent
    }.await()
}

The root of that hierarchy is usually the CoroutineScope. We could visualize that hierarchy as follows:

Coroutines are executed in a task hierarchy. The parent can be either a CoroutineScope or another Coroutine.

Parent CoroutineContext:

In the task hierarchy, each coroutine has a parent that can be either a CoroutineScope or another coroutine. However, the result CoroutineContext of a coroutine can be different from the CoroutineContext of the parent since it’s calculated based on this formula:

Parent context = Defaults + inherited CoroutineContext + arguments

Where:

  • Some elements have default values: Dispatchers.Default is the default of CoroutineDispatcher and “coroutine” the default of CoroutineName.

  • The inherited CoroutineContext is the CoroutineContext of the coroutine that created it.

  • Arguments passed in the coroutine builder will take precedence over those elements in the inherited context.(Examples below)

Note: CoroutineContexts can be combined using the + operator. As the CoroutineContext is a set of elements, a new CoroutineContext will be created with the elements on the right side of the plus overriding those on the left.

E.g. (Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)

Every coroutine started by this CoroutineScope will have at least those elements in the CoroutineContext. CoroutineName is gray because it comes from the default values.

Now that we know what’s the parent CoroutineContext of a new coroutine, it's actual CoroutineContext will be:

New coroutine context = parent CoroutineContext + Job()

If with the CoroutineScope shown in the image above we create a new coroutine like this:

val job = scope.launch(Dispatchers.IO) {
    // new coroutine
}

What’s the parent CoroutineContext of that coroutine and its actual CoroutineContext? See the solution in the image below!

The Job in the CoroutineContext and the parent context will never be the same instance as a new Coroutine always gets a new instance of a Job(New Job is Green and the parent has a red one).

The resulting parent CoroutineContext has Dispatchers.IO instead of the scope’s CoroutineDispatcher since the argument of the coroutine builder overrides it.

I hope you understand all the important concepts related to coroutines, and now you can efficiently utilize them to make great applications. Make sure to Follow me for exciting future content.

0
Subscribe to my newsletter

Read articles from Sagar Malhotra directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sagar Malhotra
Sagar Malhotra