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 not comfortable with coroutine basics, I'll suggest to read through one of my previous post where I covered the fundamentals of coroutines.


Scopes

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.

ViewModelScope

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`.

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
           // Coroutine that will be cancelled 
           // when the ViewModel is cleared.
        }
    }
}

The viewModelScope by default is on Dispatcher.Main. I want to modify viewModelScope to achieve two things -

  1. 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.
  2. 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.

class MyViewModel : ViewModel(), CoroutineScope {
  
  override val coroutineContext = 
      viewModelScope.coroutineContext + Dispatchers.Default
  
  fun someF() {
    launch {
       // No need to write viewModelScope.launch 
       // and this is automatically on Dispatchers.Default
     }
  }
  
  // Since our scope uses the context from viewModelScope when it is cancelled, 
  // so is our custom scope and hence all 
  // coroutines launched inside our custom scope
}

LifecycleScope

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.

class MyFragment: Fragment() {
    
    override fun onViewCreated(view: View, state: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
               // do-some ui work
        }
    }
    
    init {
         // some helpful f()s have been 
         // provided such as launchWhenStarted. 
         // E.g.We can fragment transaction after onStart etc. 
        lifecycleScope.launchWhenStarted {}
    }
}

Coroutine cancellation

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.

runBlocking {
    val scope = CoroutineScope(Dispatchers.IO)
    val j = scope.launch {
        println("Inside parent job")
        delay(50)
        val p = launch {
            println("Inside child job")
            delay(500)
            println("Ending of child job")
        }
        p.join()
        println("Ending of parent job")
    }
    j.join()
}

Output: 
Inside parent job
Inside child job
Ending of child job
Ending of parent job

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.

runBlocking {
    val scope = CoroutineScope(Dispatchers.IO)
    val j = scope.launch {
        println("Inside parent job")
        delay(50)
        val p = launch {
            println("Inside child job")
            delay(500)
            println("Ending of child job")
        }
        p.join()
        println("Ending of parent job")
    }
    // Cancelling the parent after a 50ms delay    
    delay(50)
    j.cancelAndJoin()
}

Output:
Inside parent job
Inside child job

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.

runBlocking {
    val scope = CoroutineScope(Dispatchers.IO)
    val j = scope.launch {
        println("Inside parent job")
        delay(50)
        val p = launch {
            println("Inside child job")
            delay(500)
            println("Ending of child job")
        }
        // Cancelling the child job after a 50ms delay
        delay(50)
        p.cancelAndJoin()
        println("Ending of parent job")
    }
    j.join()
}

Output:
Inside parent job
Inside child job
Ending of parent job

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.

runBlocking {
    val scope = CoroutineScope(Dispatchers.IO)
    val j = scope.launch {
        println("Inside parent job")
        delay(50)
        val p = launch {
            println("Inside child job")
            while (true) {
                print(0)
            }
            println("Ending of child job")
        }
        // Cancelling the child job after a 50ms delay
        delay(50)
        p.cancelAndJoin()
        println("Ending of parent job")
    }
    j.join()
}

Output:
Inside parent job
Inside child job
0000000....

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.

runBlocking {
    val scope = CoroutineScope(Dispatchers.IO)
    val j = scope.launch {
        println("Inside parent job")
        delay(50)
        val p = launch {
            println("Inside child job")
            // Notice how we check that the job's isActive
            while (true && isActive) {
                print(0)
            }
            println("\nEnding of child job")
        }
        // Cancelling the child job after a 50ms delay
        delay(1)
        p.cancelAndJoin()
        println("Ending of parent job")
    }
    j.join()
}

Output:
Inside parent job
Inside child job
000000....000000
Ending of child job
Ending of parent job

Lesson learned:
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.

runBlocking {
    val scope = CoroutineScope(Dispatchers.IO)
    val j = scope.launch {
        println("Inside parent job")
        delay(50)
        val p = launch {
            println("Inside child job")
            // Notice how we now removed the isActive check
            // to demonstrate that if a scope is cancelled,
            // even an uncooperative coroutine is stopped
            while (true) {
                print(0)
            }
            println("\nEnding of child job")
        }
        p.join()
        println("Ending of parent job")
    }
    delay(50)
    scope.cancel()
}

Output:
Inside parent job
Inside child job
00000.....000000
// Notice how since the scope was cancelled, 
// the "Ending of..." statements aren't printed. 
// The execution was simply stopped.

Handling 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.

val job = launch {
    try {
        // some work with a file
    } finally {
        // close the file here
    }
}
job.cancelAndJoin()

Timeout handling

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.

withTimeout(1000) {
    repeat(100) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}

Output:
I'm sleeping 0 ...
I'm sleeping 1 ...
Exception in thread "main" 
kotlinx.coroutines.TimeoutCancellationException: Timed out 
waiting for 1000 ms

Switching context

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:

val customDispatcher = 
    Executors.newSingleThreadExecutor().asCoroutineDispatcher()

launch(customDispatcher) {
    println("Thread name: ${Thread.currentThread().name}")
    withContext(Dispatchers.IO) {
        delay(100)
        println("Thread name: ${Thread.currentThread().name}")
    }
    println("Thread name: ${Thread.currentThread().name}")
    println("Here to remind you that withContext 
             suspends the calling coroutine i.e. stops 
             execution of calling f()")
}

Output:
Thread name: pool-1-thread-1
Thread name: DefaultDispatcher-worker-1
Thread name: pool-1-thread-1
Here to remind you that withContext suspends the calling 
coroutine i.e. stops execution of calling f()

Next part

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.