Instructions
Coroutines
This lesson is about coroutines. Before you start coding, you need to understand what coroutines are. A coroutine is a piece of code that can be suspended and resumed. It’s important to understand that a coroutine is not a thread. But it does run on a thread. A coroutine can be resumed on the same thread as it was suspended or on a different one. See the following image:
Imagine that you need to visit several places in a city. You take a taxi to the bank, spend some time there, then rent a scooter and go to a restaurant, and finally, you take a bus home. In this case, you are a coroutine, and the taxi, the scooter, and the bus are the threads.
While you’re getting things done in the bank and eating in the restaurant, you’re not traveling, you’re suspended. The taxi, the scooter, and the bus don’t need to wait for you. They can serve the other customers. When you’re ready to go, you resume your travel.
In some cases, you can choose several forms of transport. But sometimes you have to use a specific one. For example, if you have a long-distance trip, you have to take a bus. Traveling by scooter would be too slow. And you can’t take a taxi because it’s too expensive. In the city center, during rush hours, it may be better to use a scooter, as the bus and taxi can get stuck in traffic jams, causing the trip to take longer.
In cases where you can choose the kind of transport, it doesn’t matter which particular type of bus, taxi, or scooter serves you. In coroutines, the kinds of transport are the dispatchers. You can choose the dispatcher on which the coroutine runs, and the dispatcher gives you a thread with the desired properties. Usually, it doesn’t matter which particular instance of the thread you get.
There are some specific cases when you need to use a specific piece of the form of transport. For example, you can go to the restroom only by foot. Trying to use a bus or a taxi is impossible. And there’s only one instance of your foot. Similarly, there’s only one instance of the Android main thread.
If you keep adding more cars, buses and scooters to the city, the transport will be more efficient. But, at a certain point, traffic jams will appear, and the transport will become slower.
The number of cars, buses, and scooters in the city is limited. Similarly, the number of threads in the app is also limited. Threads are heavyweight entities. They use memory to keep their stack and CPU cycles to run the code.
On the other hand, the limit on the number of tasks you use is much higher. Tasks don’t consume any resources like roads or parking areas. Similarly, coroutines are lightweight entities. You can have thousands of them in the app at the same time, and it won’t affect performance like having thousands of threads, which could use up several gigabytes of RAM.
Suspending
Suspending is a way to pause a coroutine and resume it later on. Just like you can save a game at a checkpoint. You can then go back to that checkpoint later on. You can have multiple checkpoints, and you can go back to any of them in any order.
In Kotlin coroutines, suspending can’t happen at any place in the code. Coroutines can suspend only at suspension points. In Android Studio, there is a special icon on the left side of the editor that shows suspension points. It looks like the following:
Suspension points are invocations of suspending functions. Those functions have the suspend modifier.
The suspend modifier is a way to mark functions as suspending. As a limitation to coroutines, you can only call suspending functions from another suspending function or a coroutine. If you try to call a suspending function in a regular function, you’ll get a compilation error.
You can place the suspend modifier on a function that doesn’t have any suspension points. The code will compile, but the compiler will trigger a warning.
Building Coroutines
To start your very first coroutine in your program, you need to use one of the coroutine builders. They take a lambda as an argument, describing what block of code will run inside the coroutine. The simplest example looks like this:
runBlocking {
doSuspendableWork() // this is a suspending function
}
What’s important here is that calling the coroutine builder itself is not a suspendable operation. So, you can call it from any function. The lambda passed to the builder is a suspendable block of code, so you can call suspendable functions from it. The builder executes the lambda in the coroutine at some point in the future.
There are three basic coroutine builders in Kotlin: launch, async, and runBlocking.
runBlocking
The runBlocking builder is the simplest one. It blocks the current thread until the coroutine
completes. There are no advantages to suspensions in this case. During the period when the
coroutine is suspended, the thread is blocked. It consumes the resources but doesn’t do any useful
work.
The runBlocking builder is rarely used in production code of real Android applications.
It may be useful to integrate the suspending code with the existing blocking code, which isn’t using
coroutines. It won’t be the case when you start writing new Android projects from scratch. Most
modern, popular Android libraries are now using coroutines. You’ll learn about them in the following lessons.
Another legitimate use case of a runBlocking are simple console applications.
They are sometimes used to call the suspending functions from unit test methods. However, there’s
a dedicated runTest builder, which is more suitable for testing. You’ll learn about it in the next lesson.
launch
The launch builder is the one you’ll use most often. It starts a new coroutine and returns a
Job object. The Job is a handle to the coroutine. You can use it to check the coroutine status,
whether it’s still running, or has completed, or failed. You can also cancel the coroutine using the
Job‘s cancel method. The launch call doesn’t block the caller thread.
The launch builder requires a CoroutineScope. The CoroutineScope contains the context in
which the coroutine runs. The context is a set of properties the coroutines will run in. The properties include the dispatcher and the exception handler, among others. You’ll learn about the context elements in the next lesson.
The major feature of a CoroutineScope is that it can cancel all the coroutines started in it. The Kotlin Coroutines library provides extension functions for each Android entity having a lifecycle. For example, the Activity, Fragment, ViewModel and the composable functions all have their own scopes. If the given entity ends its lifecycle scope, it cancels all the coroutines started in it.
async
The async builder is similar to the launch builder. It also requires a CoroutineScope. The major difference is that the async builder returns a Deferred object. It’s also a Job, but it has the additional ability to return a value. The async builder is useful when you need to utilize the result of the coroutine or even multiple coroutines. With async, there’s no need to use any additional storage mechanism outside the coroutine to deal with the results. The async builder also differs from the launch builder in the way it handles exceptions. You’ll learn about this in following lessons, too.
Cancelling Coroutines
To cancel coroutines, you can call the cancel method on its Job. You can also cancel the entire scope - it will call the cancel method on all its jobs. But, often, you won’t need to do that manually. If you’re using the CoroutineScope associated with the Android entity, the coroutines library will handle the cancellations related to your lifecycle. For example, the scope obtained from the rememberCoroutineScope() function in a composable will be cancelled when the composable is removed from the screen.
Calling the cancel method doesn’t cause the immediate interruption of the coroutine. That call only changes the coroutine status to cancelled. The coroutine checks the status at every suspension point. These are the same points where the coroutine can be suspended. If it turns out that the status is cancelled a CancellationException is thrown.
The coroutine can perform some long-running operations without any I/O operations. For example, it can calculate the value of the mathematical constant Pi with many decimal places or factorials of large numbers. In such cases, there are no suspension points that could check the coroutine cancellation status.
If you are performing such operations in the coroutines, you may want to check the cancellation status manually to avoid useless computations and wasting resources. You can do that by checking the isActive property of the scope. There is also ensureActive(), which throws the CancellationException if the coroutine turns out to be canceled. It does nothing otherwise.
Note that the CancellationException is a subclass of the Exception from the standard Kotlin library. But it doesn’t mean there’s an error in the program. It indicates the normal cancellation of the coroutine. It’s ignored by the uncaught exception handler and does not cause the app to crash. It’s important to either not catch that kind of exception or to re-throw it after handling it in the catch block. If you catch the CancellationException and don’t rethrow it, the coroutine will continue its execution, possibly forever.
To sum up, we prefer the built-in coroutine scopes bound to the lifecycles of the Android entities. If you’re using your own scopes, don’t forget to cancel them when they’re no longer needed. Keep in mind that cancellation can happen only at suspension points. If you’re performing a long running operation, you may want to check the cancellation status manually. The cancellation happens by throwing the CancellationException, so don’t forget to handle it properly.