Kotlin Coroutines in practice

Nodrex
9 min readDec 7, 2023

--

Let us quickly remind ourselves couple of benefits of Coroutines

  • Lightweight
  • Suspendable
  • Structured concurrency
  • Built-in cancelation support
  • Built-in an exception handler
  • Jetpack integration
  • Most Practical

and what’s better than examples so let’s get started…

Thread usage optimization

It is a simple but very important part of the Coroutine, when we need to pause the execution of code (Suspend coroutine) we need to use the delay function like this:

CoroutineScope(Dispatchers.Default).launch {
delay(1.minutes)
log("Hello World!")
}

and we should avoid ̶T̶h̶r̶e̶a̶d̶.̶s̶l̶e̶e̶p̶ if it is not necessary, because the whole point of the coroutine is to suspend the Job (Pause execution of code) and at this time release Thread for other jobs, so as we can see delay does not sleep the thread, just pauses the job and Thread on which this job was running can be used to run other jobs, which is quite a big optimization in terms of Thread resources usage.

Context switch optimization.

Let’s imagine a scenario that an Android App needs to fetch data from a server and of course, we need to represent loaded data into UI, but data will be fetched with a background thread as usual and to show it we need to switch to the UI thread, well for coroutine it is easy, we can use withContext function like this:

CoroutineScope(Dispatchers.IO).launch {
//Network request
withContext(Dispatchers.Main){
//Data was loaded time to show it
}
//Coroutine request finished
}

well so far so good, but what if I have a permanent request to a server, we can use the same call with a while loop:

CoroutineScope(Dispatchers.IO).launch {
while (true){ //Imitating permanent request to server
withContext(Dispatchers.Main){
//Now let's make loaded data visible
}
//Loaded data was displayed, fetching data again
}
}

Can we optimize this function?
Yes we can, and particularly we need to remove withContext and replace it with another coroutine launch with the main dispatcher like this:

CoroutineScope(Dispatchers.IO).launch {
while (true){
//Imitating permanent request to a server
launch(Dispatchers.Main){
//Now let's make loaded data visible
}
//Fetching data again without waiting UI changes
}
}

But why?

Because withContext function is a simple suspendable function and our coroutine will wait for this function. Our coroutine will continue working only when this function call is finished, so the next server request will not happen immediately because of this withContext function, but if we replace this function with a new coroutine launch in this case our parent coroutine will not wait to UI and call to server will be immediate.

As we can see there is a difference between withContext(Dispatchers.Main) and launch(Dispatchers.Main).

Cooperative cancelation

Yes I mentioned in the beginning that cancelation is built into coroutines and it is true, but we need to cooperate with cancelation and it is very easy.
Let’s get back to the permanent client-server communication with while loop usage. What if the user decides to go to another view? Well, every educated developer will say that we need to cancel the coroutine that is responsible for client-server communication to release resources (I believe you will agree that this is quite a practical scenario) and those developers will be absolutely right, so how to do that?

There are 2 steps job.cancel() and cooperation in coroutine block himself, but we can choose cooperation from 3 variations:

  • Replace white(true) with built-in isActive boolean. The value of this boolean will be changed to false by the cancel() function, iteration will be finished and our coroutine will be finished also 🥳
CoroutineScope(Dispatchers.IO).launch {
while (isActive){
//Imitating permanent request to a server
}
}
  • We can use functions inside our coroutine block that are checking this isActive boolean variable internally, like the delay function. Even more realistic example is to request data from a server with delay rather than immediately so delay function is perfect for this.
CoroutineScope(Dispatchers.IO).launch {
while (true){
//Imitating permanent request to a server
delay(5.seconds)
}
}
  • The last option is to use ensureActive() function which throws CancelationException and stops coroutine. CancelationException is coroutine’s mechanism to cancel coroutine and this exception should not be treated as a regular exception, it will not crash App.
CoroutineScope(Dispatchers.IO).launch {
while (true){
//Imitating permanent request to a server
ensureActive()
}
}

Exceptions with catch 😜

Well, Coroutine has built-in exception handler like this:

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
val dispatcher = coroutineContext[ContinuationInterceptor] as CoroutineDispatcher
println("dispatcher of crashed coroutine [$dispatcher]")
}

CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
throw Exception("Test Exception by Nodrex")
}

This is a very simple and recommended way to use, but what if we need to use try catch in the coroutine block and we will cancel this coroutine?
Let’s remember ourselves that Coroutine’s cancelation mechanism is based on throwing CancelationException so if we are not catching a specific exception and we are catching parent of all exceptions like Exception class, then we need to check type of exception and rethrow it in case if it is CancellationException like this:

val job = CoroutineScope(Dispatchers.IO).launch {
while (true) {
//Imitating permanent request to a server
try {
ensureActive()
} catch (e: Exception) {
if (e is CancellationException) {
throw e
}
}
}
}
job.cancel()

But still recommended way is to use built-in CoroutineExceptionHandler

Structured concurrency 🤟

Ok so we can launch a lot of “child” coroutines under one “parent” coroutine and we have a really good so-called “child-parent” relationship (Hence the structured concurrency). Before diving into a practical example of structured concurrency let’s talk about Coroutine Scope: Scope is exactly what it says, it creates scope for coroutines and we can easily cancel all coroutines launched under this scope by canceling it, let’s enlist the most important scopes:

  • LifecycleScope — is bound to each Lifecycle object. Any coroutine launched in this scope is canceled when the Lifecycle is destroyed. A most obvious example will be Activity of course. When Activity is destroyed, this scope is canceled and all its children are canceled as well.
lifecycleScope.launch {
// Coroutine that will be canceled when the Activity is destroied.
}
  • ViewModelScope — is bound to ViewModel. Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared.
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
  • GlobalScope — is used to launch top-level coroutines that are operating on the whole application lifetime and are not canceled prematurely. So there might be examples where you need a coroutine to be running the whole App lifetime and do some stuff, but This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used. A coroutine launched in GlobalScope is not subject to the principle of structured concurrency, so I would recommend creating our own coroutine scope.
GlobalScope.launch {
// Top level coroutine that is live as long as App is a live
}
  • CoroutineScope — We can use this to create our own coroutine scope like this:
val exceptionHandler = ...

val myCoroutineScope = CoroutineScope(Dispatchers.IO + exceptionHandler)

myCoroutineScope.launch{
//Coroutine under our own scope
}

let’s say we need to launch a lot of coroutines under this scope, but now time has come and we need to cancel all coroutines, cause user navigated to another view, so beauty of structured concurrency is that we can only cancel this custom scope and all coroutines launched under this scope will be canceled

  • MainScope — this scope can be used for UI. It uses Main dispatcher and we can use this scope to launch UI-related tasks, but must be canceled manually for example in Activity’s onDestroy() function
class MainActivity {
private val uiScope = MainScope()

override fun onDestroy() {
super.onDestroy()
uiScope.cancel()
}
}

Ok so to summarize we know that we can create a coroutine scope and bind it to a specific lifecycle and canceling this lifecycle will cancel all coroutines launched under this scope and I guess you already figured out that canceling just one child coroutine will not affect others, just that one coroutine will be canceled.
What will affect all other coroutines is Exception, if Exception (I mean actual exception and not CaneclationException) is thrown from one of the coroutines, the whole scope is crashed ☹️
hmmmm what can we do? and yes there is a very simple solution

We can use SupervisorJob() which gives super-visor powers to scope 😎Let’s make small changes to our custom scope

val exHandler = ...

val myCoroutineScope = CoroutineScope(
SupervisorJob() +
Dispatchers.Default +
exHandler
)

myCoroutineScope.launch {
throw Exception("Test Exception by Nodrex")
}

myCoroutineScope.launch {
delay(2.seconds)
// This coroutine will be finished after 2 seconds no mater first one which is throwing exception
}

so as we can see we only need to append SupervisorJob to a dispatcher and exception handler. Now individual coroutine crashes will be caught in the exception handler and other coroutines will continue their journey without a problem. 👌

Now what if I want to launch coroutines inside the parent coroutine and one of them crashes? Well, the supervisor job will handle it, right? right? No, we need supervisorScope, functionally it is similar to the Supervisor job, but is used when we need to launch coroutines inside the parent coroutine and we do not want to crash all coroutines if one crashes, so here is the code:

myCoroutineScope.launch {
supervisorScope {

launch {
throw Exception("Test Exception by Nodrex")
}

launch {
delay(2.seconds)
// This coroutine will be finished after 2 seconds no mater first one which is throwing exception
}

}
}

Ok, we are going at full speed 🚀 launching coroutines, and handling exceptions. Now we want to detect when all coroutines that we launched are done. I believe this is quite a practical task as well so how can we do it?
Easily as usual happens with coroutines. So we have a Job object that launch function returns and on this Job object we have the function invokeOnCompletion

CoroutineScope(Dispatchers.IO).launch {
launch {
//Child coroutine 1
}

launch {
//Child coroutine 2
}
//parent coroutine
}.invokeOnCompletion { throwable ->
//All children coroutines are finished
}

this function will be invoked when all children coroutines are finished. This function has a throwable parameter which will be a CancelationException if Job was canceled, otherwise, if Job was finished successfully this parameter will be null.

What about all those coroutines that were launched not inside a scope, what if we need to wait for all of them and then let's say notify users about something? Well in this case we have a join function for Job to wait for that particular coroutine, but how to do it for a lot of coroutines? We need to save Job objects into the list and then to wait for all coroutines, we can use the extension function joinAll on the list

val jobList = mutableListOf<Job>()
(1..5).forEach {
jobList.add(
CoroutineScope(Dispatchers.IO).launch {
delay(1.seconds)
//Coroutine finished
}
)
}

CoroutineScope(Dispatchers.Default).launch {
//Observer Coroutine started
jobList.joinAll()
}.invokeOnCompletion {
//Observer Coroutine invokeOnCompletion
}

This is very simple and practical 🤟

Timeout & duration of coroutine

In some cases we need tasks to be done timely, if a task is delayed we do not care anymore and we want to cancel this coroutine. For example, we need to search data dynamically but we can not wait indefinitely and we need to stop coroutine after a certain amount of time. This is also easy for coroutines we have a function withTimeoutOrNull

CoroutineScope(Dispatchers.IO).launch {
val data = withTimeoutOrNull(2.minutes) {
while (isActive) {
//return data after search
}
null
}
}

In some cases, we need to measure duration to collect stats on how much time this or that calculation took. Again simple with the function measureTimeMillis

CoroutineScope(Dispatchers.Default).launch {
val duration = measureTimeMillis {
delay(5.seconds)
}
println("Coroutine duration -> $duration")
}

Conclusion

As we can see coroutines are suited for modern-day practical tasks and it is fun and easy to use. Well, there are more things in coroutines, Particularly “Async await” that I would like to talk about, but this topic is already big and I will make another topic for that so stay tuned. Meanwhile, I hope you enjoyed and my explanation about coroutines practical usage will be helpful ✌️

Here is a Github Project link that contains small code snippets about practical coroutines that might be helpful as well.

https://github.com/Nodrex/KC_Practice

--

--

Nodrex

Mankind was born on earth, but it was never ment to die here