Practical concepts for Coroutines (Part 2)
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:
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.
And now we can use it in any coroutine as follows:
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.
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.
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.
Cancellation of a child coroutine inside a supervisor job works the same a normal job.
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:
launch offers a way to handle the exception.
We pass in a CoroutineExceptionHandler object when launching a coroutine.
Note: If launch are nested then only the topmost launch will get the exception to handle. Example:
async, withContext, suspendCancellableCoroutine, coroutineScope all will bubble up the exception to the top parent coroutine.
Parent job has to handle it’s own and child jobs exceptions.
- 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.
- 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.
- 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.
- 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.
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:
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.
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:
Now to convert it into a coroutine suspend call, simply mark the function suspend return your response directly.
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.