· 16 min read
Coroutines in Kotlin 1.3 explained: Suspending functions, contexts, builders and scopes
Kotlin coroutines are one of the most exciting features in Kotlin. With them, you can simplify the work of asynchronous tasks in an impressive way, and make the code much more readable and easy to understand.
https://www.youtube.com/watch?v=fHG1mepeOCI
With Kotlin coroutines, you can write asynchronous code, which was traditionally written using the Callback pattern, using synchronous style. The return value of a function will provide the result of the asynchronous call.
How’s this magic happening? We’ll see it in a minute. But first, let’s understand why coroutines are necessary.
This content is adapted from my online training Kotlin for Android Developers, which is a training certified by JetBrains. Learn Kotlin from scratch in no time! And for a very limited time, get it with a discount of 40%. At the end of the course, you will get a certificate of completion.
Coroutines have been with us since Kotlin 1.1 as an experimental feature. But Kotlin 1.3 released the final API, and now they are production ready.
Kotlin Coroutines goal: The problem
The whole sample we’ll be using here is available on Github.
Imagine that you have a login screen like the following one:
The user enters a user name, a password, and clicks login.
Your app, under the hood, needs to do a server request to validate the login, and another call to recover a list of friends to show it on the screen.
The code in Kotlin could be something like this:
progress.visibility = View.VISIBLE
userService.doLoginAsync(username, password) { user ->
userService.requestCurrentFriendsAsync(user) { friends ->
val finalUser = user.copy(friends = friends)
toast("User ${finalUser.name} has ${finalUser.friends.size} friends")
progress.visibility = View.GONE
}
}
The steps would be:
It shows the progress
It sends a request to the server to validate the login
Then with the result, it does another request to recover the friends’ list
Finally, it hides the progress again
But things can get worse. Imagine that the API is not the best (I’m sure you’ve been there 😄), and you have to get another set of friends: the suggested friends. Then you need to merge them into a unique list.
You have two options here:
Do the second friends request after the first one, which is the simplest way but also not very efficient. The second request doesn’t need the result from the first one
Run both requests at the same time and find a way to synchronize the callback results. This is pretty complex.
In a real App, a lazy (and pragmatic) programmer would probably choose the first one:
progress.visibility = View.VISIBLE
userService.doLoginAsync(username, password) { user ->
userService.requestCurrentFriendsAsync(user) { currentFriends ->
userService.requestSuggestedFriendsAsync(user) { suggestedFriends ->
val finalUser = user.copy(friends = currentFriends + suggestedFriends)
toast("User ${finalUser.name} has ${finalUser.friends.size} friends")
progress.visibility = View.GONE
}
}
}
The code starts becoming difficult to understand, and we see the feared callback hell: the next call is done inside the previous callback, so the indentation keeps growing and growing.
Thanks to Kotlin lambdas, it doesn’t look too bad. But who knows if, in the future, you will need to add another request that makes this even more unmanageable.
Besides, remember that we took the easy path, which is also not very time effective.
What are coroutines?
In order to understand the Kotlin coroutines easily, let’s say that coroutines are like threads, but better.
First, because coroutines let you write your asynchronous code sequentially, dramatically reducing the cognitive load.
And second, because they are much more efficient. Several coroutines can be run using the same thread. So while the number of threads that you can run in an App is pretty limited, you can run as many coroutines as you need. The limit is almost infinite.
Kotlin Coroutines are based on the idea of suspending functions. These are functions that can stop the execution of a coroutine at any point and then get the control back to the coroutine once the result is ready and the function has finished doing its work.
So coroutines are basically a safe place where suspending functions won’t (normally) block the current thread. And I say normally because it depends on how we define them. We’ll see all this later.
coroutine {
progress.visibility = View.VISIBLE
val user = suspended { userService.doLogin(username, password) }
val currentFriends = suspended { userService.requestCurrentFriends(user) }
val finalUser = user.copy(friends = currentFriends)
toast("User ${finalUser.name} has ${finalUser.friends.size} friends")
progress.visibility = View.GONE
}
So in the example above, we have a common structure for a coroutine. We’ll have a coroutine builder, and a set of suspending functions that will suspend the execution of the coroutine until they have the result.
Then, you can use the result in the following line. Pretty much like sequential code. These two artifacts are the key, but take into account that coroutine
and suspended
don’t exist with those names, they’re there just so that you can see the structure without having to understand more complex concepts. We’ll see all those in a minute.
Suspending functions
Suspending functions have the ability to block the execution of the coroutine while they are doing their work. Once they finish, the result of the operation is returned and can be used in the next line.
val user = suspended { userService.doLogin(username, password) }
val currentFriends = suspended { userService.requestCurrentFriends(user) }
Suspending functions can run on the same or a different thread. It depends on how everything is set up. Suspending functions can only run inside a coroutine or inside another suspending function.
To declare your own suspending function, you just need to use the suspend
reserved word:
suspend fun suspendingFunction() : Int {
// Long running task
return 0
}
Getting back to the original example, a question you may be asking is where all this code is executed. Let’s focus on just one line:
coroutine {
progress.visibility = View.VISIBLE
...
}
Where do you think that this line will be run? Are you sure that it will be the UI thread? If it’s not, your App will crash, so it’s an important question.
And the answer is that it depends: it depends on the coroutine context.
Coroutine context
The coroutine context is a set of rules and configurations that define how the coroutine will be executed. Under the hood, it’s a kind of map, with a set of possible keys and values.
For now, it’s just enough for you to know that one of the possible configurations is the dispatcher that is used to identify the thread where the coroutine will be executed.
This dispatcher can be provided in two ways:
Explicitly: we manually set the dispatcher that will be used
By the coroutine scope: let’s forget about scopes for now, but this would be the second option
To do it explicitly, the coroutine builder receives a coroutine context as a first parameter. So there, we can specify the dispatcher that will be used. Dispatchers implement CoroutineContext
, so it can be used there:
coroutine(Dispatchers.Main) {
progress.visibility = View.VISIBLE
...
}
Now, the line that changes the visibility is executed in the UI thread. That one, and everything inside that coroutine. But what happens to the suspending functions?
coroutine {
...
val user = suspended { userService.doLogin(username, password) }
val currentFriends = suspended { userService.requestCurrentFriends(user) }
...
}
Are those requests also run on the main thread? If that’s the case, they will block it, so we would have a problem. The answer, again, it’s that it depends.
Suspending functions have different ways to define the dispatcher that will be used. A very helpful function that the Kotlin coroutines library provides is withContext
.
withContext
This is a function that allows to easily change the context that will be used to run a part of the code inside a coroutine. This is a suspending function, so it means that it’ll suspend the coroutine until the code inside is executed, no matter the dispatcher that it’s used.
With that, we can make our suspending functions use a different thread:
suspend fun suspendLogin(username: String, password: String) =
withContext(Dispatchers.Main) {
userService.doLogin(username, password)
}
The code above would still keep using the main thread, so it would block the UI, but that can be easily changed by specifying a different dispatcher:
suspend fun suspendLogin(username: String, password: String) =
withContext(Dispatchers.IO) {
userService.doLogin(username, password)
}
Now, by using the IO dispatcher, we use a background thread to do it. withContext
is a suspending function itself, so we don’t need to use it inside another suspending function. Instead, we can do:
val user = withContext(Dispatchers.IO) { userService.doLogin(username, password) }
val currentFriends = withContext(Dispatchers.IO) { userService.requestCurrentFriends(user) }
You may be wondering what dispatchers we have and when to use them. So let’s clarify that now!
Dispatchers
As we saw, dispatchers are coroutine contexts that specify the thread or threads that can be used by the coroutine to run its code. There are dispatchers that just use one thread (like Main) and others that define a pool of threads that will be optimized to run all the coroutines they receive.
If you remember, at the beginning we told that 1 thread can run many coroutines, so the system won’t create 1 thread per coroutine, but will try to reuse the ones that are already alive.
We have four main dispatchers:
Default: It will be used when no dispatcher is defined, but we can set it explicitly too. This dispatcher is used to run tasks that make intensive use of the CPU, mainly App computations, algorithms, etc. It can use as many threads as CPU cores. As these are intensive tasks, it doesn’t make sense to have more running at the same time, because the CPU will be busy
IO: You will use this one to run input/output operations. In general, all tasks that will block the thread while waiting for an answer from another system: server requests, access to database, files, sensors… As they don’t use the CPU, you can have many running at the same time, so the size of this thread pool is 64. Android Apps are all about interaction with the device and network requests, so you probably will use this one most of the time.
Unconfined: if you don’t care much what thread is used, you can use this one. It’s difficult to predict what thread will be used, so don’t use it unless you’re very sure of what you’re doing
Main: this is a special dispatcher that is included in UI related coroutine libraries. In particular, in the Android one, it will use the UI thread.
You have now the power to control the elements, use it wisely :)
Coroutine Builders
Now that you’re able to change the execution thread in a breeze, you need to learn how to run a new coroutine. To do that, you’ll use the coroutine builders.
We have different builders depending on what we want to do, and you could technically write your own. But for most cases, the ones that the library provides are more than enough. Let’s see them:
runBlocking
This builder blocks the current thread until all the tasks inside that coroutine are finished. That goes against what we want to achieve with Kotlin coroutines. So what’s the use then?
runBlocking
is very helpful to test suspending tasks. In your tests, wrap the suspending task you want to test with a runBlocking
call, and you will be able to assert the result and prevent that the test finishes before the background task ends.
fun testSuspendingFunction() = runBlocking {
val res = suspendingTask1()
assertEquals(0, res)
}
But that’s it. You probably won’t use runBlocking
for much more than that.
launch
This is the main builder. You’ll use it a lot because it’s the simplest way to create coroutines. As opposed to runBlocking
, it won’t block the current thread (if we use the proper dispatchers, of course).
This builder always needs a scope. We’ll see scopes in the next section, but for now, let’s just use the GlobalScope
:
GlobalScope.launch(Dispatchers.Main) {
...
}
launch
returns a Job
, which is another class that implements CoroutineContext
.
Jobs have a couple of interesting functions that can be very helpful. But it’s important to know that a job can have a parent job. That parent job has some control over their children, and that’s where these functions come into play:
job.join
With this function, you can block the coroutine associated with that job until all the child jobs are finished. All the suspending functions that are called inside that coroutine are tied to this job, so when the job can find out when all those child jobs finish and then continue the execution.
val job = GlobalScope.launch(Dispatchers.Main) {
doCoroutineTask()
val res1 = suspendingTask1()
val res2 = suspendingTask2()
process(res1, res2)
}
job.join()
job.join()
is a suspending function itself, so it needs to be called inside another coroutine.
job.cancel
This function will cancel all its associated child jobs. So if, for instance, the suspendingTask1()
is running when cancel()
is called, this won’t return the value to res1
and suspendingTask2()
will never be executed:
val job = GlobalScope.launch(Dispatchers.Main) {
doCoroutineTask()
val res1 = suspendingTask1()
val res2 = suspendingTask2()
process(res1, res2)
}
job.cancel()
job.cancel()
is a regular function, so it doesn’t require a coroutine to be called.
async
We have this other builder that you will see now that it’s going to fix the second important problem we had in the original example.
async
allows running several background tasks in parallel. It’s not a suspending function itself, so when we run async
, the background process starts, but it immediately continues running the next line. async
always needs to be called inside another coroutine, and it returns a specialized job that is called Deferred
.
This object has a new function called await()
, which is the blocking one. We’ll call await()
only when we need the result. If the result is not ready yet, the coroutine is suspended at that point. If we had the result already, it’ll just return it and continue. This way, you can run as many background tasks as you need.
So in the example below, the first request is required to do the other two. But both friend requests can be done in parallel. Using withContext
we are wasting a precious time:
GlobalScope.launch(Dispatchers.Main) {
val user = withContext(Dispatchers.IO) { userService.doLogin(username, password) }
val currentFriends = withContext(Dispatchers.IO) { userService.requestCurrentFriends(user) }
val suggestedFriends = withContext(Dispatchers.IO) { userService.requestSuggestedFriends(user) }
val finalUser = user.copy(friends = currentFriends + suggestedFriends)
}
If we imagine that each request takes 2 seconds, this would be taking 6 seconds (approx) to finish. If we substitute that with async
:
GlobalScope.launch(Dispatchers.Main) {
val user = withContext(Dispatchers.IO) { userService.doLogin(username, password) }
val currentFriends = async(Dispatchers.IO) { userService.requestCurrentFriends(user) }
val suggestedFriends = async(Dispatchers.IO) { userService.requestSuggestedFriends(user) }
val finalUser = user.copy(friends = currentFriends.await() + suggestedFriends.await())
}
The second and the third tasks run in parallel, so they would (ideally) run at the same time, and the time would be reduced to 4 seconds.
Besides, synchronizing both results is trivial. Just call await on both and let the Kotlin coroutines framework do the rest.
Scopes
So far we have a pretty decent code doing quite complex operations in a very simple way. But we still have a problem.
Imagine that we want to show these friends list in a RecyclerView, but while we are running one of the background tasks, the user decides to close the activity. The activity will now be in isFinishing
state, so any UI update will throw an exception.
How can we solve this situation? With scopes. Let’s see the different scopes we have:
Global scope
It’s a general scope that can be used for any coroutines that are meant to continue executing while the App is running. So they shouldn’t be tied to any specific components that can be destroyed.
We’ve used it before, so should be easy now:
GlobalScope.launch(Dispatchers.Main) {
...
}
When you use GlobalScope
, always ask yourself twice whether this coroutine affects the whole App and not just a specific screen or component.
Implement CoroutineScope
Any classes can implement this interface and become a valid scope. The only thing you need to do is to override the coroutineContext
property.
Here, there are at least two important things to configure: the dispatcher, and the job.
If you remember, a context can be a combination of other contexts. They just need to be of different type. So here, in general, you will define two things:
The dispatcher, to identify the default dispatcher that the coroutines will use
The job, so that you can cancel all pending coroutines at any moment
class MainActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
private lateinit var job: Job
}
The plus(+)
operation is used to combine contexts. If two contexts of different type are concatenated, it will create a CombinedContext
that will have both configurations.
On the other hand, if two of the same type are concatenated, it will use the second one. So for instance: Dispatchers.Main + Dispatchers.IO == Dispatchers.IO
We create the job as lateinit
so that we can later initialize it in onCreate
. It will then be canceled in onDestroy
.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
...
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
So now, code gets simpler when using coroutines. You can just use the builder and skip the coroutine context, as it will use the one defined by the scope, which includes the main dispatcher:
launch {
...
}
Of course, if you’re using coroutines on all your activities, it may be worth extracting that code to a parent class.
Extra - Convert callbacks to coroutines
If you’ve started thinking of using coroutines in your project, you’re probably wondering how you’re going to keep your current libraries, which may be making use of callbacks, into your new shiny coroutines.
suspend fun suspendAsyncLogin(username: String, password: String): User =
suspendCancellableCoroutine { continuation ->
userService.doLoginAsync(username, password) { user ->
continuation.resume(user)
}
}
This function returns a continuation
object that can be used to return the result of the callback. Just call continuation.resume
, and that result will be returned by the suspending function to the parent coroutine. It’s that easy!
Extra 2 - I know you will ask me about RxJava
Yeah, every time I mention coroutines, I get the same question: “Do coroutines substitute RxJava?“. The short answer is no.
The long answer, it depends:
If you’re using RxJava just to move from the main thread to a secondary thread, you’ve seen that coroutines can do that pretty easily. So yeah, you probably don’t need RxJava
If you make extensive use of streams, combining them, transforming them, etc, then RxJava still makes sense. There’s a thing called Channels in coroutines that might be able to substitute the most simple RxJava cases, but in general you would prefer to stick to Rx streams.
If you have deeper questions about it, then I probably can’t help you, as I just know the very basics of RxJava.
In any case, it’s worth mentioning that Kotlin has implemented a library to use Rx with coroutines, so you might be interested in taking a look.
Conclusion
Coroutines open up a world of possibilities and simplify executing background tasks in a way that you probably couldn’t imagine.
I really recommend you to start using them in your projects. If you want to review the code, the example is uploaded and available on Github.
Enjoy writing your own coroutines!
And remember! If you want to learn Kotlin easily in a structured way and in no time, I can’t stop recommending you to check my online training certified by JetBrains. And for a very limited time, get it with a discount of 40%. Maybe you can convince your boss! At the end of the course, you will get a certificate of completion**.**