Step 13749 Coroutine - Builders - Overview
Step 13749 Coroutine - Builders - Overview
14 minutes reading
As you remember, a regular non-suspending function cannot call a suspending function directly. At the
same time, many types of applications start with regular functions, so how then can we use
coroutines? The solution for this is coroutine builders.
A coroutine builder is a function that takes a suspending function as its parameter and schedules its
execution. The builder can be called from regular code, acting as a bridge between suspending and
non-suspending planes. In this topic, we'll discuss the two most common builders and learn how to use
them to perform suspending operations, both synchronously and asynchronously.
O
§1. Coroutines usage
Let's say we need to perform some large operation that suspends in multiple places, for example, get a
file from a server, save it to the disk, etc. At each step of this operation, we want to use coroutines to
have concise code without callbacks, so we need to build a parent coroutine. We want to wait till the
M
operation is complete before we can notify the user and finish the app.
1 import kotlinx.coroutines.delay
2 import kotlinx.coroutines.runBlocking
3
4 suspend fun doLotsOfWorkWithFile(file: String) = delay(5_000)
5
6 fun main() {
7 runBlocking { // this lambda is our root suspending funct
8 println("Starting coroutine.") // it can contain regular functions
9 doLotsOfWorkWithFile("a") // and suspending functions as well
10 }
11 println("Work is done!")
12 }
As you can see, our main function is not suspending, but now it compiles and runs properly: the delay
between the two outputs is 5 seconds. We can say that we are waiting for the coroutine to finish in the
regular code.
Sometimes, all our code is potentially suspending. In that case, we can wrap everything with the
runBlocking builder:
In fact, this is a common approach for simple apps or tests. But what if we need to do multiple
independent operations, for example, receive two files from two different servers? We can still run
them using blocking:
Does it work faster than regular code? No! Suspending functions are executed sequentially, and we're
waiting for the whole coroutine to finish. It will take about 10 seconds. To launch different operations
that can be suspended independently we can use another builder — launch .
O
launch is similar to runBlocking in terms of syntax but has a different purpose: it does not wait for
the coroutine to finish but immediately returns a special handler to the launched coroutine called a
Job. The coroutine itself continues working, but we can check the status or even cancel it through a
Job object. We'll talk about scopes later, now we are using GlobalScope for example purposes only.
Let's take a look:
M1
2
3
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
4
5 fun main() {
6 println("Starting")
7 val importantJob = GlobalScope.launch {
8 doLotsOfWorkWithFile("important_file") // waits for 5s
E
9 println("Important file processed") // will only print after 5s
10 }
11 val notImportantJob = GlobalScope.launch {
12 doLotsOfWorkWithFile("optional_file") // also waits
13 println("Optional file processed")
14 }
15 println("Finishing")
D
This code will not print Important file processed or Optional file processed because main
doesn't wait for the launched coroutines and returns immediately. The output would be:
Starting
Finishing
To wait for the result, we still need to block the main function:
1 fun main() {
2 println("Starting")
3 val importantJob = GlobalScope.launch {
4 doLotsOfWorkWithFile("important_file") // waits for 5s
5 println("Important file processed")
6 }
7 val notImportantJob = GlobalScope.launch {
8 delay(500) // add extra delay
9 doLotsOfWorkWithFile("optional_file") // so total wait is 5.5s
10 println("Optional file processed")
11 }
12 runBlocking { // block main until 6s delay is over
13 delay(6_000) // by this time both jobs should fin
14 }
15 println("Finishing")
16 }
Notice that we don't have to wait 10 seconds to get both files processed as we did with runBlocking .
Moreover, if we decrease the wait time from 6s to 5.1s, the app will process the important file but not
the optional one. In real life, we'd actually need to wait for a job to finish, not for a specific time, and we
can do that with join().
1 fun main() {
2 println("Starting")
3 val importantJob = GlobalScope.launch {
4 doLotsOfWorkWithFile("important_file")
5 println("Important file processed")
6 }
7 val notImportantJob = GlobalScope.launch {
8 doLotsOfWorkWithFile("optional_file")
9 println("Optional file processed")
10 }
11 // we still have to use runBlocking because join() is a suspending function
12 runBlocking {
13 importantJob.join() // join() suspends until the job is done
O
14 }
15 notImportantJob.cancel() // cancel non important job if it's not done yet
16 println("Finishing")
17 }
With this approach, we can launch multiple independent operations simultaneously and wait for their
results later, cancel them, or do something else. Job is a powerful tool here, and it provides way more
features than we can discuss in this topic: we will consider it in another one, and meanwhile, you can
M
check the official documentation to learn more.
§4. Conclusion
Let's recap what we know about coroutine builders:
Builders can start coroutines from regular code. They bridge the gap between the regular and
E
suspending functions.
To wait for the result of the suspending code, use runBlocking .
To run suspending code without having to wait for it to finish, use launch .
To control the launched coroutine and check its status, use Job that launch returns.
43 users liked this piece of theory. 4 didn't like it. What about you?
D
Report a typo