What is it for and how to use Kotlin Coroutines

How to write asynchronous and sequential codes in an easier way

Gabrielle Rodrigues

💻 Software Engineering Manager at iFood

How to write asynchronous and sequential codes in an easier way

The use of Kotlin Coroutines has become increasingly common. One of the main reasons is that it is efficient in working asynchronously as it is already part of the Kotlin language, without the need to use external libraries (such as Rx). In this article we will see the main points of Coroutines for those who want to start using it, or even for those who already use it, but still don't understand some of the concepts very well.

What is Coroutines?

Coroutines is a Kotlin feature that makes it possible to write asynchronous code more easily and sequentially, without using the Callback pattern (the famous Callback Hell). Coroutines has been available since Kotlin 1.1 as an experimental version, or since Kotlin 1.3 as a stable version.

What are Coroutines for?

Coroutines have a lower cost in creating and switching contexts compared to threads, being much more efficient. Several coroutines can run using the same thread, and as many coroutines as necessary can be created, unlike threads where use is limited.

In the Kotlin documentation, an example of code is shown that creates 100 thousand coroutines (in a loop) and every coroutine displays a dot (println(“.”)). Running this code only took 1 second and all 100 thousand points were displayed. And they challenge anyone who wants to do the same with threads. What happens to threads? Possibly we would have many Out of Memory.

Now that we know what Coroutines is, let's understand the main concepts.

Suspend functions

Functions suspend (declared as suspend fun) are functions that can be suspended without blocking the thread. That is, a function suspend can be paused and resumed without blocking the current thread.

Let's analyze the difference between functions blocking (blockers) and suspending. A function is blocking when it only releases the thread in which it is executing after finishing everything, while a function suspend can pause during its execution so that another function can execute on the same thread. When this second function ends, the first (which is the suspend) runs again.

Underneath the scenes, a suspend function is a regular function (i.e. without the suspend) but with one more parameter of the type Continuation . Continuation is an interface with two methods for summarizing: one for summarizing when it was successful and the other for summarizing when there was an error.

Making a comparison with regular functions, a regular function has two common operations: invoke (or call) and return. Coroutines have these operations, but they also have more: suspend It is resume.

  • suspend: Pauses the execution of the current coroutine, saving all local variables
  • resume: Continues execution of a coroutine that was suspended, from the point at which it paused (suspended)

An important point about suspend functions is that they can only be called by others suspend functions or by a coroutine, but if you forget this, the IDE will alert you at compile time.

				
					private suspend fun getWeatherDataByCity(cityName: String) { getWeatherData(cityName) }
				
			

There are two basic functions to create a coroutine: launch It is async. In both functions it is necessary to pass a context (called Dispatchers) in which your coroutine will execute (main thread, IO, etc.). We'll talk about this context soon, but let's first see how the launch It is async.

launch

launch will create a coroutine according to the context that is passed (Dispatchers). The function launch will return a type Job.

				
					private suspend fun getWeatherDataByCity(cityName: String) { CoroutineScope(Dispatchers.IO).launch { // do what you need here } }
				
			

async

async will also create a coroutine according to the context (Dispatchers) that is passed. What differentiates the async of launch is the return type of these functions. We saw that the launch returns a job, while the async returns a Deferred. O Deferred has the method await(), which when called will wait for the coroutine to return. Therefore, whatever you do just below the await will only be executed after this coroutine returns.

				
					private suspend fun getWeatherDataByCity(cityName: String) { CoroutineScope(Dispatchers.IO).async { // do what you need here } }
				
			
				
					private suspend fun getWeatherDataByCity(cityName: String) { val deferredResult = CoroutineScope(Dispatchers.IO).async { // do what you need here } deferredResult.await() }
				
			

runBlocking

runBlocking is a coroutine function to which we don't pass any context (Dispatchers) and because of this, your code will run on the main thread. It blocks the thread continuously until it completes its execution. Therefore, Kotlin, in its documentation, strongly recommends that runBlocking is not used by a coroutine, and is only recommended to be used in main functions and in tests — in tests we can prioritize using other options before runBlocking such as the runBlockingTest.

Dispatchers (“context”)

We said that coroutine can suspend itself when we declare a function like suspend, but it is Dispatcher that knows how to resume (“resume”) this coroutine. Let's see then the types of Dispatchers that can be used:

  • Main: uses the UI (user interface) thread. Therefore, it is only recommended to use when you really need to interact with the user interface;
  • IO: used for input/output operations. It is generally used when you need to wait for a response, such as: requests to a server, reading and/or writing to a database, etc.;
  • Default: used for CPU intensive uses like list sorting, JSON parsing, DiffUtils, etc;
  • Unconfined: for operations that don't need a specific thread. It is recommended to use when it does not consume CPU time or update shared data (such as the user interface) confined to a specific thread. The coroutine that uses this Dispatcher is executed in the same thread as the one who called it, but it only remains in that thread until the first suspension point (first suspend fun). Once suspended, it is summarized in the thread.

At various times we need to switch between contexts (Dispatchers) of coroutines. Let's imagine that we make a request to the server (using Dispatchers.IO) and what returns will be sent to a LiveData (possibly using Dispatchers.Main for communicating with the UI). This example is very simple and basic, but it illustrates the possible problem of context switching. To solve cases like this, we use the withContext.

withContext

It is a function that receives a dispatcher which will be used to execute the code. For the example mentioned above, we could solve it as follows:

				
					private suspend fun getWeatherDataByCity(cityName: String) { CoroutineScope(Dispatchers.IO).launch { val weatherData = getWeatherData(cityName) withContext(Dispatchers.Main){ someLiveData.value = weatherData } } }
				
			

Conclusion

Coroutines is an efficient and practical feature for working with asynchrony. At iFood, we use Coroutines a lot these days and an impactful use case was what we did when starting the application. We made some libraries initialize in the background and in parallel, whereas before they were initialized synchronously and in the main thread. With this, we were able to reduce application startup time using Coroutines.

I leave below some links with more details about the implementation of Coroutines. Happy studying! 🙂

Was this content useful to you?
YesNo

Related posts