Coroutines on Android
In this article I’ll try to explain why coroutines are useful and why would you want to use them in your project. After we go through the most important concepts of coroutines, I’ll show you how I used coroutines to implement networking and background processing in the example project.
First of all, you might ask yourself, why we want to use coroutines over threads? Main problem with threads is that they are resource intensive, meaning it takes a lot of resources to start a thread, stop a thread. Meanwhile, coroutines are lightweight threads, since they use thread pools. Another benefit of coroutines is that they greatly simplify asynchronous code. Callbacks and synchronisation are very easy to use. In fact, they make parallel programming look very much like sequential programming. Coroutines can be paused and resumed at any time, on a number of threads. And lastly, since coroutines are based on a few fairly easy to grasp concepts, their syntax is simple and easy to use. Here are the main concepts we need to explain about coroutines:
Scope
Coroutine scope, as its name says, defines a scope for new coroutines. Every coroutine builder (like launch and async) is an extension on CoroutineScope and inherits its coroutineContext to automatically propagate all its elements and cancellation.
Context
Coroutine context represents the context of its scope. Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope. Scope provides a context in which the coroutine runs (state of the coroutine which provides variables, functionality of the coroutine etc.).
Suspending functions
Suspending functions are functions that can be run in a coroutine. They make callbacks seamless. They can be run in a coroutine (can be suspended) and that is why they can provide functionalities which have to be run in parallel.
Job
Job is a handle on that coroutine (on the piece of code which runs in the background). A launch() call returns a Job which allows us to manipulate the coroutine lifecycle. Jobs live in the hierarchy of other Jobs both as parents or children.
A job has the following states:
Most important methods of a job are cancel method which cancel it and join, which joins this job to the current thread. If a job is cancelled, all its parents and children will be cancelled too.
Dispatcher
Dispatcher determines which thread or thread pool the coroutine runs on. Different dispatchers are available depending on the task specificity. For example, default dispatcher is designed for very CPU intensive processing tasks. Common dispatchers are main — for updating UI, default — for data/image processing (CPU intensive work), IO — designed for network communication or reading/writing files and unconfined dispatcher which starts the coroutine in the inherited dispatcher that called it. If you simply want a dispatcher which will create a new thread for you, you can use newSingleThreadContext(“MyThread”) which will force creation of a new thread. This shouldn’t be used that often since threads are very expensive and coroutines are lightweight so try to use coroutines as they are intended and allow dispatcher to move around from thread to thread.
Coroutine builders
There are two main coroutine builders, launch and async. As it is described earlier, launch returns a job which is placed in jobs hierarchy. Async is another way to start the coroutine. It is just like a launch method, except it returns a result in the form of a Deferred. When we need the value, we can call await() (it’s a blocking call). If the value is available, it will return immediately and if not, it will pause the thread until it is.
Suspending functions
Coroutines build upon regular functions by adding two new operations. In addition to invoke (or call) and return, coroutines add suspend and resume.
- suspend — pause the execution of the current coroutine, saving all local variables
- resume — continue a suspended coroutine from the place it was paused
A suspending function is just a regular Kotlin function with an additional suspend modifier which indicates that the function can suspend the execution of a coroutine.
suspend fun getWeather(cityId: Int): Forecast
You can only call suspend functions from other suspend functions, or by using a coroutine builder like launch
to start a new coroutine.
withContext
A function that is very commonly used in coroutines is withContext(). It allows us to easily change the context or basically to easily switch between dispatchers. If we have let’s say, some heavy image processing, we would do it with default dispatcher. When the outcome is completed, we would want to switch to the main dispatcher in order to update the ui with the resulting image. The best way to do this is withContext function since it is very lightweight. A way to do this is:
Exception handling
Exception behaviour depends on the coroutine builder. We have two coroutine builders: launch and async. Launch generates a job and async generates deferred result. Since launch creates a job hierarchy, the exception will propagate throughout the hierarchy and will immediately cause job to fail. The way of handling exceptions is to use try catch block inside of the launch block or use structure called exception handler.
On the other side, handling exception with async method is slightly different. Async method doesn’t provide hierarchy, it just provides outcome. The exception is deferred, just like the result and it is not shown until is consumed. If we don’t consume the result, the exception won’t be thrown. So, in this case, await call should be wrapped with the try catch block.
Example project
In the example project, I’ll try to demonstrate how you can use coroutines with most commonly used library for network operations (Retrofit) to pull the data from the remote server and process it locally. Project is hosted on the GitHub (https://github.com/stevan-milovanovic/coroutines) and it shows the weather forecast for the city in which the user is currently located.
Retrofit and Coroutines
Retrofit is a type-safe HTTP client for Android and Java. Since it provides built-in support for Kotlin suspend modifier on functions, we can easily define our interface:
ViewModel and Coroutines
A CoroutineScope keeps track of all coroutines it creates. Therefore, if you cancel a scope, you cancel all coroutines it created. This is particularly important if you’re running coroutines in a ViewModel.
If your ViewModel is getting destroyed, all the asynchronous work that it might be doing must be stopped. Otherwise, you’ll waste resources and potentially leaking memory. If you consider that certain asynchronous work should persist after ViewModel destruction, it is because it should be done in a lower layer of your app’s architecture. For context tied to the viewModel lifecycle, ViewModel extension is created called viewModelScope. In this example, I am using viewModelScope to launch both network operation and local file processing, although I am using IO Dispatcher for execution.
Testing Coroutines
For testing coroutines, I made custom test rule named TestCoroutineRule so that everything can be executed on the same thread:
and this is how it is used for writing a test:
Thanks for reading. Checkout this example in GitHub.