DEV Community

Rafi Panoyan
Rafi Panoyan

Posted on

Basic key behaviors of Kotlin coroutines

When I first learned about Kotlin coroutines, I was having a hard time identifying the specific behavior of all keywords: launch, withContext, async, etc...

Is my coroutine suspended ? How can I wait many of them at one point ? Where are they executing ?

Quickly, I decided to sum up some key basic behaviors to help me with that, and that's what I will share today ! This post is aimed at developers who are not using coroutines right now, or are using it but need a quick refresh on some basic behaviors.

What is a coroutine ?

According to the offical documentation, a coroutine is essentially a (very) light-weight thread.
It enables asynchronous operations without the need of callbacks, Promise, or Future concepts. Write imperative code, run concurrent operations.

Snippets setup

Let's declare some context for our coroutines snippets:

private val job = Job()
private val customScope = CoroutineScope(job + Dispatchers.Default)

private suspend fun fakeHardWork(tag: String): String {
    println(tag)
    delay(4000)
    return "I'm exhausted"
}
  • A Job is responsible for holding a reference to a coroutine. With that reference you can .start() the coroutine if it was not started, wait for its completion with .join() or cancel it with .cancel()
  • customScope is a CoroutineScope composed of the job and a CoroutineDispatcher (and it can be much more). Yes, I added a CoroutineDispatcher to a Job. That's because they both are a CoroutineContext and they can be merged together.
  • fakeHardWork() is, as the name can suggest, a suspend function faking a 4 seconds task

I did not get into the details of these objects that are part of the coroutine's vocabulary. I encourage you to read the documentation about all of them, or find some posts describing them.

Quick look at CoroutineDispatcher

The CoroutineDispatcher is responsible for describing where the coroutine must be executed. Some dispatchers are provided by the core library with these properties :

Finally, Dispatchers.Main property is a special dispatcher that is allocated to main thread operations. Such tasks as manipulating the UI are often required to be executed on this dispatcher (Android crashes the app if that's not the case for example).

Using the Dispatchers.Main property without providing a concrete implementation in the classpath of your application will throw an IllegalStateException. kotlinx-coroutines-android is the provider of the Main dispatcher for Android.

Launching a coroutine

fun newCoroutine() = 
    customScope.launch { // starts a new coroutine 'C1'

        fakeHardWork("working") // this suspends C1 until the function completes
        println("done")
    }

Theoretical output:

00s:0000ms : working
04s:0000ms : done

This piece of code is quit straighforward. We launch a new coroutine inside the customScope. Since this scope has been initialized with Dispatchers.Default, it's executing on the corresponding thread pool.

launch creates and executes a new coroutine and must be called on an existing CoroutineScope.

Switching Dispatchers

fun switchDispatchers() = 
    customScope.launch { // starts a new coroutine 'C1'
        fakeHardWork("work from Dispatchers.Default") // same as above, suspends C1

        // switch Dispatcher, still suspends C1
        withContext(Dispatchers.IO) { 
            fakeHardWork("work from Dispatchers.IO") 
        }
        println("done on Dispatchers.Default")
    }

Theoretical output:

00s:0000ms : work from Dispatchers.Default
04s:0000ms : work from Dispatchers.IO
08s:0000ms : done on Dispatchers.Default

Here, withContext changes the execution context of the block in parameter. The second call to fakeHardWork() is executed on Dispatchers.IO, and withContext is still suspending C1.

withContext merges the current CoroutineContext it has been launched from (here customScope's context), with the context you passed in parameter.

withContext does not create a new coroutine and suspends its caller until its block is executed.

Parallel execution inside a coroutine

fun parallelWorkInCoroutine() = 
    customScope.launch { // starts a new coroutine C1

        // fire a new coroutine C2. C1 is not suspended
        val work_1 = async { fakeHardWork("w_1 from Dispatchers.Default") }
        // fire a new coroutine C3 on another Dispatcher. C1 is not suspended
        val work_2 = async(Dispatchers.IO) { fakeHardWork("w_2 from Dispatchers.IO") }

        // await() calls suspend C1
        val result = work_1.await() + work_2.await()
        println(result)
    }

Theoretical output:

00s:0000ms : w_1 from Dispatchers.Default
00s:0010ms : w_2 from Dispatchers.IO
04s:0010ms : I'm exhaustedI'm exhausted

Now this is interesting. async is a coroutine builder. Unlike withContext, it actually creates and starts a new coroutine while also immediatly returning a Deferred<T> (T being the return type of the block passed to async, a String in our example).

We can then call await() on this object to effectively suspend the caller coroutine (C1 in the snippet) and get back the String produced by the blocks passed to async.

Here result equals to "I'm exhaustedI'm exhausted".

With this, we could await many Deffered and then execute something only when all of them complete.

async creates and starts a new coroutine. The returned Deferred allows us to suspend the caller coroutine and get the produced value.

Wrapping it up

I described very few behaviors about coroutines, there's so much more to discover and to learn about them ! But I find these points to be good tools to start manipulating coroutines and learn step by step.

I'll probably write later about other more advanced concepts. For now, if you were asking yourself if coroutines are worth a try, go ahead !

More starting resources

If you are curious, this great talk is a perfect introduction to Coroutines from the creator, Roman Elizarov.

Top comments (0)