A small guide to migrate asynchronous code to Coroutines

Yamal Al-Mahamid
Making Tuenti
Published in
9 min readNov 16, 2021
https://pixabay.com/photos/migratory-birds-geese-wild-geese-508013/

Usually, there are few opportunities where we can start a new project from scratch and choose from the beginning which technologies we want to use. In general, we work in projects with a great projection in time where we need to adapt to past decisions.

However, that is not an excuse to not try to evolve our application internals to avoid ending with a ton of obsolete code full of suppressed deprecation warnings.

In this post, I’m going to talk about how we introduced Kotlin Coroutines in our app, the new asynchrony standard for Android, and handy tips for other people who are in the same situation.

Where do we come from? We have been working in the past years with a promise based system as the way to handle asynchrony in our app (We also use RxJava, but that is another story…). Promises basically consist in an object wrapping callbacks for success and error cases. Something like:

Where jobDispatcher is a custom class containing different thread pools one for each type of job (disk, network, computing and UI). Then in the caller code, we subscribe to that promise with a fluent interface:

Where done and fail are callbacks executed in the specific thread pool determined by the first param of that callback (using the same previous “JobDispatcher”).

As you can see, this is not as bad as it sounds, this has been our approach for some years and it has worked (and still works) very well. But some drawbacks about this system are:

  • It gets a little messy when multiple callbacks are nested (due to its asynchronous syntax)
  • Our custom “JobDispatcher” is built on top of an external library with no more maintenance (the author even recommends to migrate to Coroutines!)

Also given that coroutines are the way to go (recommended approach by Google for Android apps) and they are not just a lib but the own language asynchrony system, we decided that it was time to include them in our app. And our first task was to understand what is a Coroutine and clarify the main concepts around them:

  • A coroutine is a subroutine which execution can be stopped and resumed
  • When a coroutine is stopped, it is said that it has been suspended
  • When a coroutine is suspended, the thread is not blocked
  • Multiple coroutines can run in the same thread

With that in mind, our next step was to choose a small and straightforward feature of the app and replace its Promise implementation with a Coroutine based one. For simplicity I’m using a sample code with a flow similar to our real scenario (I’m skipping the UI in this example):

The first thing we need to do before migrating the above code is to spend at least two minutes explaining some basic concepts. There are a lot of terms in the coroutines documentation that one might ignore and still being able to use coroutines without problem but knowing just a few things about the internals can save us hours of debugging:

Suspending function

Suspending functions are normal functions which execution can be paused and then resumed later. And all of it without blocking any thread. A function only needs a suspend modifier to become a suspending function. Suspending functions can only be invoked from another suspending function or from a coroutine. Some examples of scenarios where a function can be suspended are:

  • Some coroutine is running on the main thread and then runs some work on an IO thread. The code executed on main thread is suspended while the IO code runs. When the latter ends, main thread code is resumed.
  • Every time delay function is called within a coroutine, it is suspended until the delay completes.

Context

Just a set of elements that define the coroutine. These are two main elements:

  • Job: the job that holds the coroutine process with its own lifecycle which has two final states: cancelled or completed.
  • CoroutineDispatcher: the entity which determines which thread or pool of threads should the coroutine run in. Types: Main, IO, Default, Unconfined or a custom one (I will come to this custom CoroutineDispatcher later).

Scope

The coroutine scope as the name states is the scope where the coroutine is executed. It is just an object that holds an instance of a coroutine context used to create new coroutines. A coroutine can only be created using a scope and using one of the builders such as launch or async.

When a new coroutine is created, it inherits the context of the scope it was created from. Using the same CoroutineDispatcher by default but creating a new Job. In this way, we build a parent-child relationship between coroutines created inside each other.

Now that we know how to create coroutines, let the migration begin! The first we need is to create a Coroutine scope so we can create coroutines, and we are creating it in our ViewModel:

It is important to keep a reference to the parent Job of the scope so we can cancel every coroutine started in this view (e.g.: an Activity) after it is destroyed, we don’t want to waste resources. When canceling a job, every child job is canceled as well. In this case we are using SupervisorJob, which warrantees that if a child job of a coroutine fails, its siblings and parent won’t be cancelled, they are independent (this doesn’t occurs with jobs of type Job).

Note: With Jetpack lifecycleScope and viewModelScope, this cancelation does not need to be handled by the developer since those libs already cancel their jobs for us, but we think it is important to know first what is going on.

Next thing we need to change is the returned type of the use case and repository: we must keep both different types of response (success and error) but we can only have one single return type. In this case Either is a good choice since it was already being used across all the app (other solution could have been to use a Sealed class with two subtypes). If you don’t know what Either is, take a look at the Arrow implementation.

We have also removed the JobDispatcher, but now if our ViewModel calls this use case, it will be executed on Main thread even with coroutines. We need to run this code in another thread, but keeping the previous behavior, that is, run only the code inside getData in that other thread. To accomplish that we take advantage of withContext function:

What is happening here? withContext suspends the coroutine, executes its block of code under a new context (in this case, in a new IO thread thanks to Dispatchers.IO), and then when the result is returned, the coroutine is resumed. No UI has been frozen due to this change of context!

Note that suspend keyword must be added to the method signature to indicate that this method can be suspended as part of a coroutine. Therefore, this keyword needs to be added to the use case method as well (Remember: non-suspend methods can’t call suspend ones).

Now, it’s time to create the coroutine and replace the promise callbacks with some sequential code in the ViewModel:

Let’s do some recap: With launch, a new coroutine is created and executed on Main thread because the Coroutine scope was initialized with Dispatchers.Main. When getData method from the repository is called, that coroutine is suspended and the code inside withContext is executed in a different IO thread. When the repository code ends, the result is returned to the initial coroutine and its execution is again resumed in the Main thread. Since suspension is not blocking, Main thread is not blocked, so no ANRs to worry about!

Asynchronous code that couldn’t be migrated

Some parts of our code couldn’t be migrated, because the interface couldn’t be changed (external lib) or because the size of the refactor needed to accomplish that task wasn’t worth it. In those scenarios, the not migrated code looked like this:

We couldn’t get rid of that interface based on callbacks, but that is not a problem since Coroutines have a method called suspendCancellableCoroutine that will suspend current coroutine and execute a lambda that will receive a Continuation object, a reference to the suspended coroutine which include a method to resume it (among others methods). So, an approximation of the migrated code would look like this:

Non-UI dependent work

If there is work that doesn’t depend on any view and it’s going to be executed from a coroutine, then a different scope from ViewModel’s should be used, so the job isn’t cancelled when an Activity/Fragment is destroyed.

Kotlin Coroutines already provide a scope that doesn’t need to be instantiated. Its name is GlobalScope and it will be kept active as long as the application is alive. But it is recommended to have a different scope with custom configuration (specifying a CoroutineDispatcher, custom CoroutineExceptionHandler, etc). It is useful also if coroutines created with it need to be cancelled at some point.

In this example we are calling await after creating the coroutine because a view might want its result but since it was created with a separated scope, it won’t be cancelled when the view’s scope is cancelled at the time view is destroyed.

This scope’s parent job doesn’t have to be cancelled because it is now bound to the whole application, it will be destroyed as soon as the app does. However, there are scenarios where we want to cancel the coroutines created with it in use cases such as a log-out. In our case, we don’t want to have any background task running after the user logs out. To do so, we created our custom application-level scope implementation:

Some things here:

  • SupervisorJob: to have independent coroutine jobs.
  • runBlocking: In our code, the part in charge of log-out is not yet migrated to Coroutines and we need to wait for every coroutine to end (after cancelling them) using the suspend function join. However, we need to call it from a non-coroutine code (blocking). runBlocking allows us to invoke suspending code from blocking one.

Note: This custom and global scope should be instantiated as Singleton and provided in those classes that need to do that kind of work that doesn’t depend on any view.

Make your code testable

In this post I’m not covering how to test Coroutines, but I’d like to remark at least one thing: Inject Coroutine Dispatchers, do not hardcode them.

Unit tests should be deterministic and execute always in the same way. If you use, for instance, Dispatchers.IO directly in your class when creating a coroutine, it can lead you to non-deterministic behavior when executing your tests: you can’t determine when that coroutine will run. If you would have injected an instance of CoroutineDispatcher in your class, you would have be able to replace it in your unit test with TestCoroutineDispatcher, a special CoroutineDispatcher that allows immediate execution of coroutines.

Our DI framework is Dagger 2 and our approach to inject different types of Dispatchers is the following:

Custom Dispatchers

In an ideal world, we would have used the Dispatchers provided by the Coroutines library and do not worry more about threads. But we live in the real world, where things like technical debt exist.

Our mentioned “JobDispatcher” is based on Java Executors and for reasons that are out of the scope of this article, we needed to run our Coroutines in the same thread pools used by those Executors. But, in the end, Coroutines should be agnostic about the threading implementation they run in, that’s why we are not worried about not having used standard Dispatchers.

But how did we do that? Fortunately, Coroutine creators though about this scenario when implementing Dispatchers: there is a Java Executor extension function that converts them into Coroutine Dispatchers, and that function is asCoroutineDispatcher.

Having understanding that, we only needed to change the way we were injecting Dispatchers in our classes (also we have changed the name of the Dispatchers just to keep naming consistency):

Where NetworkExecutor is our class that inherits from Java Executor.

There is much more work that is already being done, such as introducing Flow or Jetpack Lifecycle components (so we don’t have to handle scope creation and cancellation). This article is just an overview to demonstrate that introducing Coroutines in big or old projects is not a task as hard as it looks and we encourage everyone to give it a try.

Thanks for reading and it would be nice if you can share similar experiences in the comments!

--

--