Originally published at https://www.chaddha.me/practical-concept-coroutines-1/ on Dec 26, 2020.
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.
This article covers the practical concepts for using coroutines in actual projects, different use-cases that arise, whie showing the code in action to explain the concepts clearly.
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.
Table of content
- Coroutine cancellation
2.1 Parent job is cancelled
2.2 Child job is cancelled
2.3 Child job co-operation
2.4 Scope is cancelled
2.5 Handling resources
- Timeout handling
- Switching context
- Next part
Kotlin provides GlobalScope to us — a scope lasts the lifetime of an app. Coroutines on this scope run like daemon threads — they die with the application. For android architecture components, we already have some scopes provided.
A ViewModelScope is defined for each ViewModel in your app. Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared.
We access this scope by the variable `viewModelScope`.
The viewModelScope by default is on Dispatcher.Main. I want to modify viewModelScope to achieve two things -
- It runs on background thread by default i.e. I don’t have to write ‘viewModelScope.launch(Dispatcher.Default)’ every time. Since most of the time, my primary use case is doing network calls, or computation or curation in view model.
- I don’t want to write ‘viewModelScope.launch’ every time. It’s too verbose.
Remember Coroutine scope? Well in code, it is represented by an interface called ‘CoroutineScope’. To achieve a cleaner code with the above two mentioned points, we implement this interface and make our custom scope with the same context as viewModelScope but a different dispatcher. This means our custom scope has the same life as the viewModelScope. Here we show the practical usage of mixing a child’s coroutine context with a parent’s content.
For lifecycle based components like fragment or activity, we have lifecycle scope.
A LifecycleScope is defined for each Lifecycle object. Any coroutine launched in this scope is cancelled when the Lifecycle is destroyed. You can access the CoroutineScope of the Lifecycle either via `lifecycle.coroutineScope` or `lifecycleOwner.lifecycleScope` properties.
You can cancel a job if it’s taking too long or the requested operation isn’t required anymore, or for fun. To understand cancellation cases clearly, let’s consider we have a scope in which we start a parent coroutine. Inside it we start a child coroutine.
Parent job is cancelled
When a parent job is cancelled, it cancels all it’s child jobs as well. This shows that they imitate the OS world parent-child process relationship.
Child job is cancelled
When a parent job cancels it’s child. It is expected that the child will stop while the parent will continue executing.
Child job co-operation
But what if the child was doing some kind of a loop.
Since we are launching our child coroutine using launch, it won’t block the parent coroutine and will execute on another thread from the Dispatchers.IO it inherits from it’s parent.
What happened here is that the child job didn’t check if it was cancelled and kept executing. Then why did it work previously? Because of the delay(500) provided by the coroutines library.
Since the framework is built with the following thought in mind -
“If we cancel a job (either via cancelling the child job or cancelling the parent job), we want it to stop executing” — Any suspend f() in the coroutine library checks that it the job isActive before executing.
So in the previous example when delay(500) got over, it checked whether the job it was executing for was alive before continuing. To fix this there are two solutions: Using isActive or yeild. I like isActive better.
If the job is cancelled, the isActive flag is made false.
Child jobs should be co-operative enough to notice if they are cancelled, then they don’t execute unnecessarily.
Scope is cancelled
When we cancel a scope (either a custom scope or one tied to a lifecycle e.g. viewModelScope getting cancelled due view model onCleared being called), all the coroutines running inside the scope are instantly cancelled as well.
In other words, any work going on in any suspend f() in this scope is stopped. This is only gaurenteed for the coroutines in this scope.
E.g. if we use the viewModelScope to launch coroutines in repository and cache layer, and while they are ongoing, the scope is cancelled, the coroutines or the suspend f() calls in repository and cache layer are stopped, given that they were running in the same scope. This way we are always ensured we aren’t doing tasks that are no longer required and thus freeing up resources.
There are cases where we’ll need to handle closing resources when our job is cancelled. For this use a try finally block. Coroutine will invoke the finally block in case of cancellation too.
Do it in a 1000 microseconds or move on. We’ve seen such requirements at some point. These are timeout cases where we want to cancel a job if it’s taking too much time. There is a neat little way to do it.
I hate to do context switching in real life. Not in coroutines.
Sometimes while doing a task, we may need to do some IO operation, then some DB operation or some kind of computation, and then end with showing some UI on main thread. This require us to jump between correct dispatchers.
To do this coroutines provide us ‘withContext()’ operator. Using this we can switch to a different dispatcher and then come back to the old dispatcher as it’s block ends.
Remember, calling withContext will suspend the calling f() until the withContext block doesn’t end.
i.e. the f() execution won’t proceed until withContext is done executing. Example:
In the next part, we cover converting callback to coroutines, custom coroutine scope, supervisor job and scope, exception handling, structured concurrency and integration with retrofit.