Dive into Kotlin Coroutines.

Anatolii Frolov
ProAndroidDev
Published in
8 min readNov 21, 2023

--

Photo by Adrien Delforge on Unsplash

Currently, the use of Kotlin сoroutines to perform asynchronous tasks is becoming standard in Android development. In this article, I’ll give some basic information on how сoroutines actually work under the hood. This knowledge will give you an understanding of why it is worth taking a closer look at this technology and using it for an asynchronous approach in your projects. And if you already use them, this will allow you to better understand how the code works.

To better understand why we should choose сoroutines over pure threads, we will perform some operations using both approaches.

Problem definition

In the Android environment, all actions interacting with the user interface are performed on the main thread, also known as the UI thread. For example, we want to change the text in the text field or display a toast, etc. The main problem is that if we want to perform some long-running synchronous operation (for example, downloading a file from the network) on the main thread, this can lead to it blocking. When the UI thread of an Android app is blocked for too long, an “Application Not Responding” (ANR) error is triggered. Read more about ANR here.

We’ll check it with the first example.

Below is the function code. In this context, it doesn’t matter to us what kind of work is being done there, but it is long and synchronous. These last two factors are important to us because this will ultimately lead to undesirable consequences.

The code for the main activity is also given. It’s standard, and I won’t need much explanation for you to understand it, but I’ll still leave a couple of comments.

  • this.runOnUiThread allows you to run code on the main thread.
  • measureTimedValue is a helper function that will allow us to measure the execution time of the code.

In this example, we run a long-running synchronous task on the main thread and want to get the result, which we’ll display in a toast. At the same time, we’ll log some information about the execution of the code.

If we look at the log, we’ll see that it took 9685.0434 ms to receive the data, and all this time, the main thread was blocked.

This example was useful to us in order to once again reinforce the information that you never need to run long synchronous operations on the main thread.

But what if we execute the code in the background thread? Perhaps this will be a solution, and we won’t need Kotin Coroutines. We’ll see if this is true using another example.

In this example, all the code remains the same, but the thread on which we run it has changed; in this case, it is the background thread.

As we’ll see from the log, the main thread was not blocked, and the data was received. We can be happy, but no. When using this approach, we again encountered a problem since, as indicated above, interaction with user interface elements should occur exclusively on the main thread. Naturally, we received an exception, and the application crashed.

What if we use Kotlin Coroutines and try to run the same code? Will this achieve our goal of receiving data and displaying it in a toast?

Using coroutines

First, we need to add the necessary dependencies to Gradle.

In the code below, we create a coroutine scope and run the code inside the coroutine builder. More information about launching coroutines can be found at the link.

In this example, we launched a coroutine in a background thread and tried, as in the example with threads, to receive data and use it when displaying toast. But coroutines are not a workaround compared to threads. We tried to do the same thing in terms of interacting with a UI element from a background thread, and naturally, we got the same result with an exception and the application crashing.

The full power of coroutines will be demonstrated when we execute code on the main thread in the following example. We’ll start receiving data in a separate background thread and, having received the data, display the toast in the main thread. In this case, our code will be written in a procedural style and will be executed sequentially, step by step, inside the coroutine.

We’ll change our function a little by adding the suspend keyword. This is a kind of marker for Kotlin, by which it understands that this function will suspend the coroutine.

As I wrote above, we’ll send the code to be executed in the background thread. The withContext(Dispatchers.IO) function will allow us to do this. You can read more about this function here. But we still want to display the Toast with the results of the work on the UI thread.

All our code remains the same, but in the coroutine scope constructor, we’ll place a dispatcher responsible for running code on the main thread: Dispatchers.Main.

FYI, if you use lifecycleScope or viewModelScope, they also internally use the main thread dispatcher. For the example below I will provide the code for lifecycleScope and you will see it.

Below is a log of the results of our code.

The log shows that the main thread was not blocked, code execution went to a background thread with the result returned, and, believe me, a toast with the results of the operation was also shown on the smartphone screen. This is exactly the result we wanted to achieve. Let me remind you that our goal was to run sequential code execution in a procedural style, step by step, without blocking the main thread or crashing the application.

But how is this possible? This will be the most interesting part of this article, where we’ll dive into the coroutines and see how our code works under the hood.

Under the hood

First, we’ll remove all the auxiliary code to measure the execution time of the code and output the log. We don’t need this anymore. Now our coroutine will look like the example below. Let me remind you that the receiveData() function is suspended and will return the result, which we’ll write to a variable: val data. Then we’ll use this data as a toast message.

The code is quite simple, which is good because it will allow you to better understand the topic, especially if you are taking your first steps in this direction.

Now we’ll look on decompiled Kotlin Bytecode. I specifically removed all the code and left only the functions that are called in it.

The invoke function will be called first. It’ll be launched by the CoroutineStart class inside the launch function.

The invoke function takes the suspend function and an instance of the Continuation class as input parameters. We’ll talk about this class in more detail, but for now, here is what you need to know: there are two objects that will be used when running code inside the coroutine.

We need to go back and look at the decompiled Kotlin Bytecode for the invoke function. There, we will find these two objects. If we compare, we get:

  • Object var1 is block: suspend() -> T
  • Object var2 is completion: Continuation<T>

As we remember, a total of three functions were called in the coroutine, and we looked at one of them. In turn, the remaining two functions will be called inside the invoke function: create and invokeSuspend.

A closer look shows that the create function forms an object of the Continuation class.

Moreover, this function is contained in the base class Continuation. And not only this, but, as we’ll see, the invokeSuspend function, which we’ll look at a bit later.

And finally, the third function that will be called when the Continuation class object is created is invokeSuspend. It contains all the code that the coroutine should execute. As we already understood, this function will be called by an object of the Continuation class when the coroutine starts.

The main purpose of the Continuation class is to ensure that the user interface operation (in our case, showing toast) is performed only after receiving the result of the asynchronous operation of our receiveData function. To do this, the code inside the invokeSuspend function will be split into parts, and a label variable will be added to switch between pieces of code.

There are two objects inside: Object var3 and Object var10000. In Object var10000 will be put the result of the suspended function work, and Object var3 contains a special constant: COROUTINE_SUSPENDED.

The point where the code splits into two parts is the suspend function. It and all the code before it will go into the first part. And all the code after it is in the second. Now, when calling the invokeSuspend function, the label variable will determine which part of the code will be executed. If label is 0, then the first part of the code (our receiveData function) will be executed, and if label is 1, then the second part of the code will be executed, where the result of the work will be written to the variable var10000 and then used for display in toast.

As we figured out, the invokeSuspend function will be called for the first time at the start of the coroutine and will execute the first part of the code in case = 0, which will launch the suspend function, and the invokeSuspend function will complete (return).

Before calling the suspend function, a new value of 1 will be written to the label. This is necessary so that when the result is received, the second block of code from case 1 will start working.

But a logical question arises: how will the function be called if it has completed its work?

Note that the receiveData function accepts this as a parameter. That is, the object of the Continuation class itself is passed to the suspend function.

When the suspend function finishes its work, it will take the Continuation object that was passed to it and call its invokeSuspend function. Since the value of label was previously replaced by 1, when invokeSuspend starts running again, the second part of the code from case 1 will be executed.

Actually, only two results can come from the suspend function.

  • The first one is the return of the COROUTINE_SUSPENDED constant that was mentioned before. It means that the suspend function has not completed its work. In this case, the invokeSuspend function will be completed again (return). This will continue until the second possible option comes from the suspend function.
  • The second one is if some value other than the COROUTINE_SUSPENDED constant is returned. This will be the result of the suspend function. The code will continue.

In case 1, the result of the suspend function will be written to the var10000 variable, and then the code will continue its execution. This variable, as we see in the code below, is used to display the toast, after which the function will complete its work.

Conclusion

The topic of using Kotlin Coroutines is very extensive, and covering it in one article is a difficult task. Understanding this, we were not faced with such a task. But what we did get from this article was a little more understanding of how coroutines work under the hood. This created the necessary foundation for further study of this difficult but, at the same time, interesting topic.

I hope this article has given you a better insight into Kotlin’s programming. Clap if you liked it ;)

I wish you a good coding!

--

--