Kotlin Coroutines trong Android phần 2: Getting started

Tram Ho

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:

  1. Long running task : are tasks that take a long time to execute and will block the main thread.
  2. 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:

  1. Cancel work when it is no longer needed.
  2. Keep track when the work is done.
  3. 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:

  1. The launch will launch a new coroutine under the “fire and forget” mechanism, meaning it will not return results to the caller.
  2. 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 use launch to launch a new coroutine. Since normal functions have no way to call await (remember that normal functions will not be able to call suspend functions directly), it doesn’t make much sense to use async as the coroutine launch point. We will discuss more about using async later. We will use launch to launch a new coroutine:

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:

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:

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:

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 and supervisorScope 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 and supervisorScope 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.

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.

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.

References

Coroutines on Android (part II): Getting started

Share the news now

Source : Viblo