Hi everyone, this is the second part of the series about Kotlin Coroutines in Android. Hopefully the article will bring you useful knowledge.
Content summary
In part one , we discussed the coroutine issues that we can solve together. In short, coroutines help us solve two very common problems in programming, including:
- Long running task : are tasks that take a long time to execute and will block the main thread.
- Main-safety : allows us to ensure that any suspend function can be called from the main thread.
To address these two problems, coroutines are built by adding suspend and resume to regular functions. When all coroutine in a particular thread is suspended, it will be able to do other work freely. However, coroutines by themselves do not help us to track whether the work has been completed or not. Having a large number of coroutines – hundreds or thousands – and all of them are suspended at the same time is a very good thing. At the same time, although coroutines do not consume a lot of resources, the work they perform often brings great value, such as reading files or making network requests. To be able to track thousands of coroutines manually with code is quite a difficult job. We can try to track them all manually and make sure they are completed or destroyed, but code like this will be boring and error prone. If the code is not perfect, we may lose control of coroutines, which are called work leaks . Work leak is similar to memory leak, but worse. That is when a coroutine is lost. In addition to memory usage, a work leak can resume itself to continue using the CPU, disk, or make a network request.
A leaked coroutine can consume memory, CPU, disk, or make an unnecessary network request.
To avoid the leak of coroutine, Kotlin introduced the concept of structured concurrency . Structured concurrency is a combination of language features and best practices, allowing us to track all coroutine running if done correctly. In Android, we can use structured concurrency to do three things:
- Cancel work when it is no longer needed.
- Keep track when the work is done.
- Signal error when a coroutine fails. Let’s dive into each of these tasks and see how structured concurrency helps us ensure we never lose control of the coroutine that is running and that no leak work happens.
Cancel work with scopes
In Kotlin, coroutine must be run inside something called CoroutineScope
. A CoroutineScope
keeps control of our coroutines, even when the coroutine has been suspended. Unlike the Dispatcher
we talked about in part one, CoroutineScope
not the real thing that executes our coroutines – it is the only thing that ensures we don’t lose control of the created coroutines. To ensure all coroutine is under control, Kotlin does not allow us to start a new coroutine without a CoroutineScope
. CoroutineScope
allows you to create a new coroutine with full features introduced in part one. A CoroutineScope
keeps control of all our coroutines, and it can cancel all the coroutines it controls. This feature is very suitable for Android application development, where we always want to ensure that everything started by a screen will be cleaned up when the user leaves.
A
CoroutineScope
keeps control of all our coroutines, and it can cancel all the coroutines it controls.
Starting new coroutines
It is important to note that we cannot call suspend
functions from anywhere. The suspend and resume mechanisms require that we convert from a regular function to a coroutine. There are two ways to start a coroutine, and they have different uses:
- The launch will launch a new coroutine under the “fire and forget” mechanism, meaning it will not return results to the caller.
- async will start a new coroutine and allow us to return a result with a suspend function called
await
. In most cases, we will only uselaunch
to launch a new coroutine. Since normal functions have no way to callawait
(remember that normal functions will not be able to call suspend functions directly), it doesn’t make much sense to useasync
as the coroutine launch point. We will discuss more about usingasync
later. We will uselaunch
to launch a new coroutine:
1 2 3 4 5 6 7 8 | scope.launch { // Block này sẽ khởi động một coroutine mới // "bên trong" scope. // // Có thể gọi tới suspend function fetchDocs() } |
You can simply think of launch
as a bridge that helps us move from normal functions to suspend functions. Inside the launch
body, we can call the suspend functions and ensure main safety as introduced in the previous article.
launch
as a bridge helps us to move from normal functions to suspend functions.
Note: An important difference between launch
and async
is how they handle exceptions. async
will assume we will call await
to get the result (or exception), so it will default to no exception. That means that if we use async
to launch a new coroutine, it will silently ignore the exception. Because launch
and async
can only be called on a CoroutineScope
, all the coroutines we create will always be monitored by a scope. Kotlin will not let us create an untracked coroutine, thus avoiding work leak.
Start in the ViewModel
So if CoroutineScope
keeps track of all the coroutines started by it, and the launch
creates an coroutine, exactly where do we call the launch
and to set our scope? Also, when will we cancel all coroutine started within a scope? In Android, we usually link a CoroutineScope
to a screen. This allows us not to leak coroutines or perform tasks for Activity
or Fragment
that users no longer need. When the user redirects to start the screen, CoroutineScope
associated with that screen can cancel
all jobs.
Structured concurrency ensures that when a scope is destroyed, all of its coroutine will be destroyed.
When using coroutine along with Android Architecture Components, we will want to launch
coroutines inside the ViewModel
. This is a natural position where every job is started and you don’t have to worry that rotating the screen can destroy all coroutine. To be able to use coroutine in ViewModel
, we can use viewModelScope
extension property from lifecycle-viewmodel-ktx:2.1.0-alpha04
. Take a look at the following example:
1 2 3 4 5 6 7 8 9 | class MyViewModel(): ViewModel() { fun userNeedsDocs() { // Khởi động một coroutine mới trong ViewModel viewModelScope.launch { fetchDocs() } } } |
viewModelScope
will automatically cancel any coroutine triggered by this ViewModel
when the ViewModel
is cleared (when callback onCleard()
is called). This is often the right behavior – if we do the work when the user has closed the application, we are simply wasting the user’s battery wasting on requests. And for added security, a CoroutineScope
will spread itself. Therefore, if one coroutine we start over another coroutine, both of these coroutines will be terminated with the same scope. Therefore, when we need a coroutine to be executed when a ViewModel
exists, use viewModelScope
to switch from the regular function to coroutine. From there, viewModelScope
will automatically cancel
the coroutine for us. It is possible to write an endless loop without creating a leak, as in the following example:
1 2 3 4 5 6 7 8 9 10 11 | fun runForever() { // Khởi động một coroutine mới trong ViewModel viewModelScope.launch { // Huỷ bỏ khi ViewModel bị clear while(true) { delay(1_000) // do something every second } } } |
By using viewModelScope
, we can ensure all work, including an infinite loop, will be canceled when it is no longer needed.
Keep track of work
Starting a new coroutine is a good thing – and most of the code is all we need to consider. Launch a coroutine, make a network request, and write the results to the database. However, sometimes we need some more complicated things. For example, we may need to make two network requests simultaneously in one coroutine – to do this we need to start more coroutine. To create more coroutine, any suspend function can be done using another builder called coroutineScope
or its cousin, suspervisorScope
. This API is really confusing. coroutineScope
and CoroutineScope
are completely different things even though they only differ by 1 letter in their names. Launching a new coroutine anywhere is a way to create potential work leaks. Callers may not be aware of this new coroutine and will not control the work. To solve this problem, structured concurency will help us. Specifically, it provides a guarantee that when a suspend function is returned, all its work is done.
Structured concurrency ensures that when a suspend function is returned, all of its work is completed. Here is an example of using
coroutineScope
to fetch 2 documents:
1 2 3 4 5 6 7 | suspend fun fetchTwoDocs() { coroutineScope { launch { fetchDoc(1) } async { fetchDoc(2) } } } |
In this example, two documents are fetched from the network at the same time. The first document is fetched in a coroutine that starts with launch
, ie “fire and forget” – meaning it will not return results to the caller. The second document is fetched with async
, so this document can be returned to the caller. This example may seem a bit strange, since usually we can use async
for both documents – but what I want is to show you how we can combine launch
and asycn
depending on what you want.
couroutineScope
andsupervisorScope
allow you to safely start coroutine from the suspend function.
However, note that this code never waits for other new coroutines. It looks like fetchTwoDocs
will return when the coroutine is running. To implement structured concurrency and avoid work leaks, we want to make sure that when a suspend function, such as fetchTwoDocs
returns, all its work is done. That means the coroutine launched by it must be completed before fetchTwoDocs
returns. Kotlin makes sure that this job will not leak initialize fetchTwoDocs
with coroutineScope
builder. coroutineScope
builder will suspend itself until all the coroutine boots inside it are completed. Therefore, there is no way to return fetchTwoDocs
until all coroutine started within the coroutineScope
builder is completed.
Lots and lots of work
We have learned how to control one or two coroutines. Next, let’s all-in to try to control 1000 coroutine. Take a look at the illustration below: This example is about making 1000 network requests at a time. This is not recommended in practice on Android apps – your application will consume a lot of resources. In this code, we launched 1000 coroutine with the wrong launch
on the coroutineScope
builder. We can see everything is linked together. Since we are in a suspend function, the code somewhere must use a CoroutineScope
to initialize a coroutine. We don’t know anything about that CoroutineScope
, it could be viewModelScope
or another CoroutineScope
defined somewhere. No matter which scope calls, the coroutineScope
builder will use it as the parent to create a new scope. Then, within the coroutineScope
block, the launch
will launch new coroutines within the new scope. Until the coroutine launch launch
is completed, the new scope will control them. Finally, once all the coroutine has been started inside the coroutineScope
complete, loadLots
will be free to return. Note: the parent-child relationship between scope and coroutine is created by the Job object. But normally we don’t need to go too deep into this.
coroutineScope
andsupervisorScope
will wait for all child coroutines to complete.
There are a lot of things going on inside – however it is important that with the use of coroutineScope
and supervisorScope
, we can safely launch a new coroutine from any suspend function. Although it creates a new coroutine, we will not let the leak work happen because we will always suspend the caller until the new coroutine is completed. Another interesting thing is that coroutineScope
will create a sub scope. Therefore, if the parent scope is canceled, it will transmit this signal to all of the coroutine children. If the caller is looking at viewModelScope
, all 1000 coroutines will automatically be canceled when the user navigate away from the screen. Great. Before moving on to the error section, we should also take some time to compare between coroutineScope
and supervisorScope
. The main difference between them is that a coroutineScope
will cancel whenever a child fails. Therefore, if one network request fails, all other requests will be immediately canceled. Therefore, if you want to continue other requests even if one of them fails, we can use the supervisorScope
. A supervisorScope
will not cancel other requests when one request fails.
Signal errors when a coroutine fails
In coroutine, the error is signaled by returning an exception, similar to other normal functions. Exceptions from a suspend function will be returned to the caller via resume. As with normal functions, we are not restricted to try / catch to handle errors and we can build an abstraction so we can handle errors with different styles if we want to. However, there may be cases where we will leave errors in coroutine.
1 2 3 4 5 6 7 8 9 | val unrelatedScope = MainScope() // Ví dụ về việc sót lỗi suspend fun lostError() { // async without structured concurrency unrelatedScope.async { throw InAsyncNoOneCanHearYou("except") } } |
Note that this code declares an unrelated coroutine scope to run a new coroutine without structured concurrency. Notice that at the outset, I mentioned that structured concurrency is a combination of types and programming practices, and declaring an unrelated coroutine scope inside a suspend function is a non-compliance. Follow structured concurrency programming practices. The error is missing in this code because async
assumes that we will always call await
where it will return an exception. However, if we never call await
, the exception will not be handled.
Structured concurrency ensures that when a coroutine fails, the caller or its scope is notified. If we use structured concurrency for the above code, the error will be returned to the caller properly.
1 2 3 4 5 6 7 8 | suspend fun foundError() { coroutineScope { async { throw StructuredConcurrencyWill("throw") } } } |
Because coroutineScope
will wait for all its children to complete, it may receive a notification when they fail. If a coroutine started by coroutineScope
throws an exception, coroutineScope
will be able to return it to the caller. By using coroutineScope
instead of supervisorScope
, it will also immediately destroy all child coroutines when returning an exception.
What’s next?
In this article, we started with coroutine in Android with ViewModel
and how to work with structured concurrency to make our code better. In the following article, we will talk more about using coroutine in real cases. Thank you for taking the time to read your article.