Canceling Nested Kotlin Coroutines With CoroutineScope

Coroutines are one of the ūüėć nicest language features in Kotlin. They provide a rather headache-free way to use the power of concurrency in your Kotlin program. I was very excited when I started playing around with them but eventually¬†came across a problem: what if you have nested coroutines (like a parent-child coroutine).

For instance, I came across this problem when I wanted to launch a coroutine and with-in of that coroutine I want to start multiple coroutines and when I cancel the parent coroutine the children needs to be canceled as well. How!

Gradle Stuff

Kotlin coroutine requires us to add some more stuff to our application modules build.gradle file. We start by adding the following dependencies right after the android section:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0-RC1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0-RC1'

In this article, I assume that you’ve some elementary proficiency with Kotlin coroutine. Now instead of directly jumping to above¬†problem, I wanna start this article by just launching a simple coroutine and cancel it when the ViewModel is going to destroy.

class RepositoryViewModel constructor(private val movieRepo : MovieRepo) : ViewModel() {
   
   private var movieRepoJob : Job? = null    
   
   fun getMovies() {
      movieRepoJob = launch {
           movieRepo.getPopularMovies()
           .... update view state
      }   
   }

   override fun onCleared(){
      movieRepoJob?.cancel()
   }
   
}

You see that’s its a very simple example, we have a ViewModel and in that there’s a getMovies method who simply¬†launch¬†a coroutine. Well, actually the¬†launch¬†returns a¬†Job¬†and that¬†Job¬†allows you to cancel it while it’s been running. So, you can simply cancel the¬†coroutine in the onCleared¬†method of ViewModel. Simple isn’t it! ūü§ó

Parent-Child Async Tasks

Now let’s get back to our previous example where we have more than one async tasks running. We have one parent¬†launch¬†block and inside that block, there’s another¬†async¬†task is running. So, how do we keep track of those tasks effectively when our ViewModel is going off the screen or we need to cancel them. Using this solution our previous example would look like:

class MyViewModel constructor(private val apiService : ApiService) : ViewModel() {
   
   private var parentJob : Job? = null    
   private var firstChildJob : Job? = null
   private var secondChildJob : Job? = null
   
   fun getMovies() {
      parentJob = launch {
           apiService.request1()
           firstChildJob = launch {
                apiService.request2()
                ........
           }
           secondChildJob = launch {
                apiService.request3()
                ........
           }
      }   
   }

   override fun onCleared(){
      parentJob?.cancel()
      firstChildJob?.cancel()
      secondChildJob?.cancel()
   }   
}

This above code works for our contrived example¬†but its hackiness makes me go ‚ÄúICK!‚ÄĚ. Currently, we’ve three¬†launch¬†and we have to keep track of three¬†Jobs¬†and, maybe later we need to have four¬†launch¬†and we need to keep track of four¬†Jobs. It gets little unwieldy and a bit too much code to actually maintain.¬†Trying to make this style work in any nontrivial program would be a complete nightmare.

To make the above example work in a conical way we need to have something like CompositeDisposable who can hold multiple other Disposable and dispose of them if any error occur or canceled them all with just one method. Happily! the kotlin coroutine library gives us the CoroutineScope.

Kotlin CoroutineScope

A CoroutineScope makes error handling easier, If any child coroutine fails, the entire scope fails and all of children coroutines are canceled. It should be implemented on entities with a well-defined lifecycle that is responsible for launching children coroutines. By entities, I mean Activity, Fragment, and ViewModel.

Every coroutine builder like launch and async are an extension of CoroutineScope and inherits corutineContext to automatically propagate both context elements and cancellation.

There are two ways to use to CoroutineScope in an application.

  • First, is to implement the¬†CoroutineScope on entities.
  • Second to use the coroutineScope top-level function which creates a new CoroutineScope and calls the specified¬†suspend¬†block with this scope.

1. Cancel Jobs By Implementing Kotlin CoroutineScope

Back to our previous parent-child coroutine example. Using CoroutineScope our example code looks like this:

class MyViewModel constructor(private val apiService : ApiService) : ViewModel(), CoroutineScope {   // 1
   // 2
   private val job = Job()
   // 3
   override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main
   // 4
   fun executeCalls() {
      launch(context = coroutineContext) {
           val firstRequestDeferred = async {
                apiService.request1()
           }
           val secondRequestDeffered = async {
                apiService.request2()
           }
           handleResponse(firstRequestDeferred,await(),secondRequestDeffered.await())
      }   
   }
   // 5
   override fun onCleared(){
       job.cancel()
   }   
}

Below is the explanation of the above code:

  1. As I said, we need to implement CoroutineScope so that our launch and  async invoked in the scope of an entity.
  2. We created this Job instance so that we can easily cancel all async call whenever our ViewModel is going to destroy.
  3.  We need to tell in which dispatchers our response is needed to dispatch.
  4. Note that an implementation of launch also defines an appropriate coroutineContext. Now the parent coroutine waits for all the coroutines inside their block to complete before completing themselves. If the ViewModel is going to destroy or any of the launched coroutines throws an exception then all the nested coroutines are canceled.
  5. Cancel Job in ViewModel onCleared method. After destroying, all children jobs will be canceled automatically.

Note:¬†You see with the above approach we don’t need to keep track so many Jobs¬†if we cancel the parent coroutine or any Exception¬†thrown inside any of the coroutines then all jobs will be canceled.

2. Cancel Jobs With CoroutineScope Top-level Function

// 1
suspend fun executeCall(apiService : ApiService) = couroutineScope {
   val firstDeferredRequest = async {
         apiService.request1()
   }
   val secondDeferredRequest = async {
        apiService.request2()
   }
   handleResponse(firstRequestDeferred.await(),secondDeferredRequest.await())
}

// 2 
val job = launch {
    executeCall()
}

// 3
job.cancel()

With structured concurrency, you have to wrap your code into a coroutineScope block that establishes a boundary of your operation, its scope. All the async coroutines become the children of this scope and, if the scope fails with an exception or is canceled, all the children are canceled, too.

If you wanna know how the kotlin suspended function works under the hood, do check out this article => Explore How Kotlin Coroutine Works Under The Hood.

That is all for now!

If you like this article, I appreciate your support to share this article with others to let the great Android and Kotlin community know. I’m just a beginner like you guys. I write what I learned. Please let me know what did I miss in this article.

Thank you for being here and keep reading…

LEAVE A REPLY

Please enter your comment!
Please enter your name here