This article covers the practical concepts for using coroutines in actual projects, different use-cases that arise, while showing the code in action to explain the concepts clearly.

If you are looking for some concepts not covered here - check the first part. If you are not comfortable with coroutine basics, I'll suggest to read through one of my previous post where I covered the fundamentals of coroutines.


Converting callback to coroutines

Many times we are using libraries which doesn’t support coroutines yet, for that we need to wrap the callback into a suspend f(). Coroutine library provide an extension f() ‘suspendCancellableCoroutine.

It takes in a suspend block it’ll execute. Just like withContext it’ll suspend the calling coroutine until we invoke resume f()s from inside it.

Lets consider an example:

interface Callback {
    fun onSuccess(result:Int)
    fun onFailure(e: Exception)
}
fun someTask(callback: Callback) {
    Thread.sleep(1000)
    // Written together for brevity but only one will occur
    callback.onSuccess(2)
    callback.onFailure(Exception("I failed"))
}

Here we have a callback interface and some f() that does some asynchronous work for us and takes a callback.

Lets see how we can wrap someTask in a coroutine world easily.

suspend fun someTaskWrapped() = suspendCancellableCoroutine<Int> {
    it.invokeOnCancellation {
        // Cancel any task or handle resources that you want
    }
    someTask(
        object:Callback{
            override fun onSuccess(result: Int) {
                it.resume(result)
                // Resume the calling coroutine with this result
            }
            override fun onFailure(e: Exception) {
                it.resumeWithException(e)
                // Resume the calling coroutine with an exception 
                // And let it handle the failure correctly
            }
        }
    )
}

And now we can use it in any coroutine as follows:

fun main() {
    runBlocking {
        // Exception handling ignored for now
        launch(Dispatchers.Default) {
            println(someTaskWrapped())
        }
    }
}

Custom coroutine scope

If we need to create a custom coroutine scope in a function, we can use the ‘coroutineScope(block: suspend CoroutineScope.() -> R): R’ f () provided by the coroutine library. coroutineScope() doesn’t block the calling thread when it launches a new coroutine but it’ll suspend the parent job.
i.e. Just like withContext the calling f() doesn’t execute further until coroutineScope block doesn’t finish executing.

fun main() {
    runBlocking {
        launch(Dispatchers.Default) {
            println("Hello1")
            a()
            println("Hello2")
        }
        println("Hello3")
    }
}
suspend fun a() = coroutineScope {
    println("Hello4")
    delay(100)
    println("Hello5")
}

Output:
Hello3 
Hello1
Hello4
Hello5
Hello2

// Note how Hello3 is printed before Hello1 since launch isn't blocking 
// the main thread
// 
// Also note how Hello2 is printed after Hello5 
// since coroutineScope suspends the calling f() execution
// 
// It's just that runBlocking will wait for all it's child to 
// complete before completing itself while 
// it blocks the thread it was called on.

I’ve been unable to find a use-case where I have to make a custom coroutine scope like this that I can’t achieve via normal builders. If you know any use-cases for it, please comment.

Supervisor job and scope

A fact that i’ve not revealed until now is that, if there is an exception in any child job of a parent job, then the parent job is cancelled as well.

For e.g. if parent1 had three child jobs - child1,child2, child3.
If child1 fails with an exception, then parent1 will also fail causing child2 and child3 to stop as well.

But what if we don’t want parent1, child2 and child3 to fail if child1 does?
For that we can use a SupervisorJob instead of a normal job as parent.
SupervisorJob ensures that if one of it’s child fails with exception, then others keep on executing.

runBlocking {
    val ceh = CoroutineExceptionHandler { _, throwable ->
        println("Failure with ${throwable.localizedMessage}")
    }
    val scope = CoroutineScope(SupervisorJob()+Dispatchers.IO+ceh)
    val j = scope.launch {
        val c1 = scope.launch {
            throw Exception("Exception1")
        }
        val c2 = scope.launch {
            delay(100)
            println("C2 still executing")
        }
        joinAll(c1, c2)
    }
    j.join()
}

Output:
Failure with Exception1
C2 still executing
Output if SupervisorJob isn't used:
Failure with Exception1

Bonus: ViewModelScope by default uses a SupervisorJob, due to which if one operation is met with a failure, the other child coroutines aren’t affected.

A supervisorScope is a way to start a coroutine with supervisor job. The calling coroutine is suspended until this block completes just like withContext. It can’t be cancelled from outside since it’ll suspend the calling coroutine.

For example:

runBlocking {
    val ceh = CoroutineExceptionHandler { _, throwable ->
        println("Failure with ${throwable.localizedMessage}")
    }
    supervisorScope {
        launch(ceh) {
            throw Exception("Exception1")
        }
        launch {
            delay(100)
            println("C2 still executing")
        }
    }
    println("After supervisor scope")
}

Output:
Failure with Exception1
C2 still executing
After supervisor scope

Cancellation of a child coroutine inside a supervisor job works the same a normal job.

Exception handling

When doing operations inside coroutines exceptions might occur.

Different coroutine builders have different ways of handling it but broadly it can be broken as two behaviour:

1st Behaviour

launch offers a way to handle the exception.
We pass in a CoroutineExceptionHandler object when launching a coroutine.

val eh = CoroutineExceptionHandler { _, throwable ->
    println("Oops error occured: ${throwable.localizedMessage}")
}
launch(eh) {
   delay(100)
   throw Exception("I failed")
}
Output:
Oops error occured: I failed

Note: If launch are nested then only the topmost launch will get the exception to handle. Example:

val eh = CoroutineExceptionHandler { _, throwable ->
    println("Oops error occured: ${throwable.localizedMessage}")
}

// Crashes the main thread since top most launch doesn't 
// handle exception even if child coroutine does
launch {
	delay(100)
    launch(eh) {
		throw Exception("I failed")
    }
}

// Doesn't crashes the main thread and the exception is caught
launch(eh) {
   delay(100)
   launch {
   		throw Exception("I failed")
   }
}

2nd Behaviour

async, withContext, suspendCancellableCoroutine, coroutineScope all will bubble up the exception to the top parent coroutine.

val eh = CoroutineExceptionHandler { _, throwable ->
    println("Oops error occured: ${throwable.localizedMessage}")
}

launch(eh) {
    async {
        delay(100)
        throw Exception("I failed)
    }
    
    withContext(Disptatchers.IO) {
        delay(100)
        throw Exception("I failed)
    }
    coroutineScope {
        delay(100)
        throw Exception("I failed)
    }
    println(callbackOp())
}

fun callbackOp() = suspendCancellableCoroutine<Int> {
  delay(50)
  throw Exception("I failed)  
}

Output:
(Output for individual run of async, withContext, coroutineScope or 
suspendCancellableCoroutine block mentioned above. Shown together for brevity)
Oops error occured: I failed

To summarize:
Parent job has to handle it’s own and child jobs exceptions.

Notable points

  1. Since the most sensible way is to start coroutine via launch, if we catch the exception at the top most launch we can ensure that no exception passes through. This works for both normal job or supervisor job.
  2. CoroutineExceptionHandler won’t recieve cancellation exceptions.
    When a child coroutine is cancelled manually by a parent, a cancellation exception can be given when calling cancel(). It is expected of the parent job that if it is cancelling any of it’s child, then they should handle the situation themselves.
  3. For a normal parent job, if multiple exceptions are thrown only the first one is reported with others as suppressed.
    In case of Supervisor job, all exceptions are thrown one by one since siblings aren’t cancelled if one child throws an exception.
  4. A normal parent job is given exception to handle only after all childs are complete.
    Supervisor job is given it’s child exceptions immediately since sibling childs won’t be cancelled.

Structured concurrency

The aim of structured concurrency put in simple terms is easy -

Any coroutine we launch — we have to ensure that it doesn’t execute if it’s not required.

There are numerous cases where the requirement of coroutines becomes invalid, but let’s consider the two most commons ones to understand the concept:

Parent job is cancelled

The simple way to ensure this in any coroutine we launch inside our parent coroutine is to handle for cancellations.

Let’s consider a code:

val parentJob = scope.launch {
    val result1 = suspendFunction1()
    var nonCooperativeChildResult = 0
    val nonCooperativeChild = launch {
        while(isActive) {        
             nonCooperativeChildResult = 
                 someRecursiveProcessing(nonCooperativeChildResult)
        }
    }
    val result2 = async { someApiCallWithRetrofit() }
    val result3 = callbackConvertedWithSuspendCancellable()
    var combinedResult = result1 + result2.await() + result3
    nonCooperativeChild.cancel()  
    combinedResult += nonCooperativeChildResult
    processResult(combinedResult)
}
// On cancel button clicked we can do either of the following parentJob.cancel()
scope.cancel()

We have to think on the following points in case of scope or parent job cancellation:

  • Any suspend f() is automatically cancelled when scope of parent job is cancelled. i.e. no need to handle anything for suspendFunction1()
  • In case of scope cancellation, nothing executes anymore which means no worries for uncooperative child. But with a parent job cancellation, we have to ensure that our child coroutines are cooperative. e.g. nonCooperativeChild checks for isActive flag.
  • Functions provided by coroutines library like await() already check for parent job cancellation which makes our job easier.
  • Handle cancellation when converting callbacks to coroutine code using suspendCancellableCoroutine instead of suspendCoroutine.
    Since suspendCoroutine suspends on the calling coroutine, if the calling one is cancelled before it completes, this coroutine shouldn’t try to resume the original caller. Using suspendCancellableCoroutine ensures that if the calling coroutine is cancelled, so your execution is stopped as well.

In summary, to ensure you only do work when required:

  • No worries for suspend f()s or async coroutines
  • Remember to use isActive if you are doing a continous loop behaviour inside child coroutines
  • Use suspendCancellableCoroutine for converting callbacks to coroutines.

Dependency sibling job failure

Sometimes we have cases where we have to combine results from multiple tasks, but if one job fails, our computation as a whole fails. Maybe we have to combine result from API1 and API2 to show some UI. If either of them fails, we want to show a failed UI.

For such cases launch a parent job with exception handling to show failed UI then use two child coroutines to get result, prefer async builder. If either of the child fails, the parent job will recieve the exception and will show the failed ui.

val ceh = CoroutineExceptionHandler { _, throwable ->
    launch (Dispatchers.Main) { showFailedUI() }
}
launch(Dispatchers.Default + ceh) {
    val apiCall1Result = async {
        val r1 = apiCall1()
        if(r1.status=="failed") {
            throw Exception("api call 1 fails")
        }
    }
    val apiCall2Result = async {
        val r2 = apiCall2()
        if(r2.status=="failed") {
            throw Exception("api call 2 fails")
        }
    }
    val combinedResult = apiCall1Result.await() + 
                                apiCall2Result.await()
    launch(Dispatchers.Main) { 
        showSuccessUI(combinedResult)
    }
}

Integration with Retrofit

Most of us use retrofit to make our network calls. We define a simple service in an interface for us to consume. Converting retrofit normal calls to coroutines is easy.
Consider the following service you are familiar with:

interface SomeApiService {
	@GET("some_end_point")
	fun getApiEndpointData(@QueryMap map:Map<String,String>): Call<ApiData>
}

val service = RetrofitHelper.createRetrofitService(SomeApiService::class.java)
// we pass the callback here
service.getApiEndpointData().enqueue(....)

Now to convert it into a coroutine suspend call, simply mark the function suspend return your response directly.

interface SomeApiService {
	@GET("some_end_point")
	fun getApiEndpointData(@QueryMap map:Map<String,String>): Call<ApiData>
}

launch(coroutineExceptionHandler) {
    val service = RetrofitHelper.createRetrofitService(SomeApiService::class.java)
    val response = service.getApiEndpointData()
    // Use the response here
}

The suspend call to our service will automatically wait for the network call to be complete. In case of any error or exception, it is thrown in the coroutine and can be caught in the exception handler we pass.


That's it :)
With all these concepts now clear, it should be easy to fit coroutines in the big picture and use it in daily work.