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.
fun receiveData(): String { | |
// Long synchronous work is performed here. | |
return "Data received" | |
} |
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.
class MainActivity : AppCompatActivity() { | |
private lateinit var binding: ActivityMainBinding | |
@OptIn(ExperimentalTime::class) | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
binding = ActivityMainBinding.inflate(layoutInflater) | |
setContentView(binding.root) | |
this.runOnUiThread { | |
val (result: String, duration: Duration) = measureTimedValue { receiveData() } | |
Log.d("TAG", "$result after ${duration.toDouble(DurationUnit.MILLISECONDS)} ms.") | |
showToast(context = this, message = result) | |
} | |
Log.d("TAG", "onCreate completed") | |
} | |
private fun showToast(context: Context, message: String) { | |
val toast = Toast.makeText(context, message, Toast.LENGTH_SHORT) | |
toast.show() | |
} | |
} |
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.
21:58:23.164 27359-27359 TAG Data received after 9685.0434 ms. | |
21:58:23.168 27359-27359 TAG onCreate completed |
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.
val thread = Thread { | |
val (result: String, duration: Duration) = measureTimedValue { receiveData() } | |
Log.d("TAG", "$result after ${duration.toDouble(DurationUnit.MILLISECONDS)} ms.") | |
showToast(context = this, message = result) | |
} | |
thread.start() | |
Log.d("TAG", "onCreate completed") |
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.
22:03:56.665 27504-27504 TAG onCreate completed | |
22:04:06.166 27504-27529 TAG Data received after 9491.5399 ms. | |
22:04:06.168 27504-27529 AndroidRuntime FATAL EXCEPTION: Thread-2 | |
java.lang.NullPointerException: Can't toast on a thread that has not called Looper.prepare() |
Job Offers
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.
dependencies { | |
... | |
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1") | |
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") | |
} |
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.
val scope = CoroutineScope(Dispatchers.IO) | |
scope.launch { | |
val (result: String, duration: Duration) = measureTimedValue { receiveData() } | |
Log.d("TAG", "$result after ${duration.toDouble(DurationUnit.MILLISECONDS)} ms.") | |
showToast(context = this@MainActivity, message = result) | |
} | |
Log.d("TAG", "onCreate completed") |
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.
22:10:09.755 27650-27650 TAG onCreate completed | |
22:10:18.776 27650-27675 TAG Data received after 9006.3558 ms. | |
22:10:18.797 27650-27675 AndroidRuntime FATAL EXCEPTION: DefaultDispatcher-worker-1 | |
java.lang.NullPointerException: Can't toast on a thread that has not called Looper.prepare() |
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.
suspend fun receiveData(): String { | |
withContext(Dispatchers.IO) { | |
// Long asynchronous work is performed here. | |
} | |
return "Data received" | |
} |
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.
val scope = CoroutineScope(Dispatchers.Main) | |
scope.launch { | |
val (result: String, duration: Duration) = measureTimedValue { receiveData() } | |
Log.d("TAG", "$result after ${duration.toDouble(DurationUnit.MILLISECONDS)} ms.") | |
showToast(context = this@MainActivity, message = result) | |
} | |
Log.d("TAG", "onCreate completed") |
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.
/** | |
* [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle]. | |
* | |
* This scope will be cancelled when the [Lifecycle] is destroyed. | |
* | |
* This scope is bound to | |
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]. | |
*/ | |
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope | |
get() = lifecycle.coroutineScope |
Below is a log of the results of our code.
22:28:45.709 28976-28976 TAG onCreate completed | |
22:29:11.505 28976-28976 TAG Data received after 25069.8728 ms. |
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.
lifecycleScope.launch { | |
val data = receiveData() | |
showToast(context = this@MainActivity, message = data) | |
} |
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.
public final Object invoke(Object var1, Object var2) | |
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) | |
public final Object invokeSuspend(@NotNull Object $result) |
The invoke function will be called first. It’ll be launched by the CoroutineStart class inside the launch function.
package kotlinx.coroutines | |
// --------------- launch --------------- | |
public fun CoroutineScope.launch( | |
context: CoroutineContext = EmptyCoroutineContext, | |
start: CoroutineStart = CoroutineStart.DEFAULT, | |
block: suspend CoroutineScope.() -> Unit | |
): Job { | |
val newContext = newCoroutineContext(context) | |
val coroutine = if (start.isLazy) | |
LazyStandaloneCoroutine(newContext, block) else | |
StandaloneCoroutine(newContext, active = true) | |
coroutine.start(start, coroutine, block) | |
return coroutine | |
} |
package kotlinx.coroutines | |
public enum class CoroutineStart { | |
@InternalCoroutinesApi | |
public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit = | |
when (this) { | |
DEFAULT -> block.startCoroutineCancellable(completion) | |
ATOMIC -> block.startCoroutine(completion) | |
UNDISPATCHED -> block.startCoroutineUndispatched(completion) | |
LAZY -> Unit // will start lazily | |
} | |
} |
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>
public final Object invoke(Object var1, Object var2) { | |
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE); | |
} |
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.
@NotNull | |
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) { | |
Intrinsics.checkNotNullParameter(completion, "completion"); | |
Function2 var3 = new <anonymous constructor>(completion); | |
return var3; | |
} |
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.
package kotlin.coroutines.jvm.internal | |
@SinceKotlin("1.3") | |
internal abstract class BaseContinuationImpl( | |
public val completion: Continuation<Any?>? | |
) : Continuation<Any?>, CoroutineStackFrame, Serializable { | |
protected abstract fun invokeSuspend(result: Result<Any?>): Any? | |
public open fun create(completion: Continuation<*>): Continuation<Unit> { | |
throw UnsupportedOperationException("create(Continuation) has not been overridden") | |
} | |
public open fun create(value: Any?, completion: Continuation<*>): Continuation<Unit> { | |
throw UnsupportedOperationException("create(Any?;Continuation) has not been overridden") | |
} | |
// There are many other functions here | |
} |
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.
int label; | |
@Nullable | |
public final Object invokeSuspend(@NotNull Object $result) { | |
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); | |
Object var10000; | |
switch (this.label) { | |
case 0: | |
ResultKt.throwOnFailure($result); | |
this.label = 1; | |
var10000 = FunctionsKt.receiveData(this); | |
if (var10000 == var3) { | |
return var3; | |
} | |
break; | |
case 1: | |
ResultKt.throwOnFailure($result); | |
var10000 = $result; | |
break; | |
default: | |
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); | |
} | |
String data = (String)var10000; | |
FunctionsKt.showToast((Context)MainActivity.this, data); | |
return Unit.INSTANCE; | |
} |
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.
package kotlin.coroutines.intrinsics | |
@SinceKotlin("1.3") | |
public val COROUTINE_SUSPENDED: Any get() = CoroutineSingletons.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).
case 0: | |
ResultKt.throwOnFailure($result); | |
this.label = 1; | |
var10000 = FunctionsKt.receiveData(this); | |
if (var10000 == var3) { | |
return var3; | |
} | |
break; |
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.
case 1: | |
ResultKt.throwOnFailure($result); | |
var10000 = $result; | |
break; | |
default: | |
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); |
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.
int label; | |
@Nullable | |
public final Object invokeSuspend(@NotNull Object $result) { | |
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); | |
Object var10000; | |
switch (this.label) { | |
// here were completed case 0 and case 1 | |
} | |
String data = (String)var10000; | |
FunctionsKt.showToast((Context)MainActivity.this, data); | |
return Unit.INSTANCE; | |
} |
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!
This article was previously published on proandroiddev.com