Kotlin coroutines

In this blog we are going to see what kotlin coroutines are and why we need to use coroutines and what problem it solves in android application development.

What is a coroutine ? 

Coroutine is a structured concurrency design pattern,  which can be used to simplify asynchronous programming. It is conceptually similar to threads, in the sense that it takes a block of code and executes concurrently with other code without blocking the main thread. However a coroutine is not bound to a single thread. It may suspend its execution in one thread and resume in another.

Why do we need to use coroutines ?

  • Coroutine suspends a function and executes in a different thread making it safe from the main thread. 
  • It provides built in support for exception handling and cancellation
  • We can chain multiple coroutines inside a coroutine scope.

What problem is solves ?

For example, let’s say we fetch data from an api and display it.

fun loadData(){
// blocks the main thread
val data = fetchData()
showData(data)
}

In the first block you can see that the network call is happening in the main thread. That means it blocks the main thread until the network call is completed. Which means it will freeze the screen until the main thread is freed. It will also produce ANR’s if the network call takes time to execute.

To avoid blocking the main thread we have the callbacks. 

fun loadData() {
// does not block the main thread
    fetchData { data ->
        showData(data)
    }
}

Inside the fetch data the network call will be executed in a different thread and once the response is received the callback will be used to return the data to the main thread.

This way we can make asynchronous calls without blocking the main thread.

But what if we want to make nested api calls. It will become complicated to maintain the code and handle exceptions.

networkCall{ response ->
networkCall { response ->
networkCall { response ->
networkCall { response ->                      // Do something
                  }
            }
      }
}

Coroutines simplify this in a way that, nesting api looks simple.

fun fetchAndShowProducts() {
    CoroutineScope(Job()).launch{
      val data = fetchData() // function suspended

      // make sequential api calls if required.
  val anotherApi = networkCall()
  val anotherApi = networkCall()

      showData(data) // back on UI thread
    }
}

Suspend fun fetchData(){
    ... do some work here
}

Suspend function

Suspend functions are regular functions with the suspend keyword. This function will be suspended from the main thread when called, that means the function will be executed without blocking the main thread and resumed when the execution is completed.

Suspend functions cannot be called directly from normal functions. It needs to be called from another suspended function or a coroutine.

Coroutine scope

Every coroutine needs a coroutine scope to execute. Coroutine scope defines the scope of the coroutine call. Coroutine scope takes a coroutine context to initialize the scope with. The coroutine context can be built with the following as given below.

val exceptionHandler = CoroutineExceptionHandler {
  coroutineContext, exception -> /* handle exception */
}

val myContext: CoroutineContext = Job() + Dispatchers.Main  + CoroutineName("my name") + exceptionHandler

val myScope = CoroutineScope(myContext)
Or 
val myScope = CoroutineScope(exceptionhandler)
  • JOB – Handles the lifecycle of the coroutine. It can be used to check whether the coroutine isActive , isCancelled or isCompleted.

Jobs are unique for each and every coroutine we create. When we create a new coroutine inside a coroutine without passing any changes in the coroutine context then it will inherits the parent coroutine context.

topLevelScope.launch {
launch { } // new job with parent context
launch(Dispatcher.IO) {} // new job + Dispatcher.IO
}

In the first case, coroutine with a new job will be created. And the remaining parent context will be inherited for the dispatcher, name and exception

In the second case, new dispatcher is passed while creating the coroutine. So this coroutine will have a new Job and dispatcher. And it also inherits the name and exception from the parent.

  • Dispatcher – Dispatches the work to the appropriate thread
    • Main – Runs in the main thread to update the UI.
    • IO – To make IO operations like api calls.
    • Default – To make long running disk operations
  • Coroutine name (optional)- It can be used to identify the coroutine by name. By default it will be “coroutine”.
  • Coroutine exception handler – We can provide an exception handler to the top level scope which will catch exceptions if any.

    Try/catch can be used inside the coroutine to catch exceptions. If try/catch is not used then the exception will be propagated to the top level exception handler. If the top level exception handler is not defined, it cannot be catched by surrounding the top level coroutine with try/catch.
val exceptionHandler = CoroutineExceptionHandler {
  coroutineContext, exception -> /* handle exception */
}
val topLevelScope = CoroutineScope(Job() + exceptionHandler)
topLevelScope.launch(exceptionHandler) { ... }

Coroutine Builders 

Once the coroutine scope is created we can use the following coroutine builder “launch()” or “async()” to start a new coroutine.

Launch vs async :

  • launch() is a fire and forget concept. It will start a new coroutine and doesn’t return any value.
coroutineScope{ launch{
  val result = fetchDocs()
}}
  • async() starts a coroutine and returns a value when execution is completed. await() can be used to get the result.
coroutineScope{ val result = async{ fetchDocs() }
// here result will have Deferred<Data>.The data is fetched but not  emitted.
 // using await() we can get the data. result.await()}

It can be used for parallel execution. Let’s say if we want to fetch data from two different api calls which do not depend on each other. 

We can also combine both launch and async to start multiple coroutines.

When a job fails and throws an exception in a coroutine, all other jobs inside the coroutine will be cancelled.

It will be useful when we don’t want to continue other jobs when one job fails. But in some cases like when we upload/download file, we expect the failure of one job should not affect other jobs.

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
  Log.e("Tag", "", throwable)
}

val scope = CoroutineScope(Job() + exceptionHandler)

scope.launch { job1() } // job completed
scope.launch { job2() } // throws exception
scope.launch { job3() } // job cancelled.

suspend fun job1() {
  delay(100)
  Log.e("TAG", "job1")
}

suspend fun job2() {
  delay(500)
  Log.e("TAG", "job2: ")
  throw Exception("Job cancelled")
}

suspend fun job3() {
  delay(1000)
  Log.e("TAG", "job3: ")
}

In the above case when job2 throws an exception the other jobs will be cancelled.

To overcome this we can use SupervisorJob.

SupervisorJob

Children of a supervisor job can fail independently of each other. A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children, so a supervisor can implement a custom policy for handling failures of its children:

  • A failure of a child job that was created using launch can be handled via CoroutineExceptionHandler in the context.
  • A failure of a child job that was created using async can be handled via Deferred.await on the resulting deferred value.

If a parent job is specified, then this supervisor job becomes a child job of its parent and is cancelled when its parent fails or is cancelled. All this supervisor’s children are cancelled in this case, too. The invocation of cancel with exception (other than CancellationException) on this supervisor job also cancels parent.

CoroutineScope(SupervisorJob()).launch {}

Now in the same case with SupervisorJob() , even though the job2 throws an exception. Other jobs will be completed without any interruption.

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
  Log.e("Tag", "", throwable)
}

val scope = CoroutineScope(SupervisorJob() + exceptionHandler)

scope.launch { job1() } // job completed
scope.launch { job2() } // throws exception
scope.launch { job3() } // job completed

We can also write the above code like this.

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
  Log.e("Tag", "", throwable)
}

val scope = CoroutineScope(SupervisorJob() + exceptionHandler)
scope.launch {
launch { job1() } // job completed
launch { job2() } // throws exception
launch { job3() } // job cancelled.
}

In this case when job2 fails, other jobs will be cancelled.

We used the supervisorJob in the scope and you can wonder why it is failing then.

Here is the catch, the supervisorJob needs to be the direct parent of a coroutine to work.

In this case when we use lauch{} inside to create a new coroutine, it will be created with Job instead of supervisorJob.

Here comes the saviour to overcome this constraint.

SupervisorScope

It creates a CoroutineScope with SupervisorJob and calls the specified suspend block with this scope. The provided scope inherits its coroutineContext from the outer scope, but overrides context’s Job with SupervisorJob. This function returns as soon as the given block and all its child coroutines are completed.

Unlike coroutineScope, a failure of a child does not cause this scope to fail and does not affect its other children, so a custom policy for handling failures of its children can be implemented.

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
  Log.e("Tag", "", throwable)
}

val scope = CoroutineScope(SupervisorJob() + exceptionHandler)
scope.launch {
supervisorScope{
  launch { job1() } // job completed
  launch { job2() } // throws exception
  launch { job3() } // job completed.
}
}

Now the above will work as expected. All the jobs will be completed even though job2 throws an exception.

Job vs SupervisorJob

We should know when to use the Job() and when to use the SupervisorJob().

When failure of one job should not affect the other jobs, then we should use SupervisorJob().

Handling exceptions

Exceptions can be handled in two ways.

  1. Using try/catch
  2. Using CoroutineExceptionHandler

Launch with coroutineScope

val scope = CoroutineScope(Job() + exceptionHandler)

// Exception handled in the root
scope.launch {
   job2()
}

// Exception handled in the try/catch
scope.launch {
   try {
       job2()
   }catch (e : Exception){
       Log.e("TAG", "chainingCall: ", e)
   }
}

// child fails and propagated to the parent
// Exception handled in the root
scope.launch {
   try {
       launch { job2() }
   }catch (e : Exception){
       Log.e("TAG", "chainingCall: ", e)
   }
}

Launch with supervisorScope

// Exception handled in the root
scope.launch {
  supervisorScope {
      job2()
  }
}

// exception handled in try/catch
scope.launch {
  supervisorScope {
      try {
          job2()
      }catch (e : Exception){
          Log.e("TAG", "chainingCall: ", e)
      }
  }
}

// child fails and propagated to the parent
// Exception handled in the root
supervisorScope {
  try {
      launch { job2() }
  }catch (e : Exception){
      Log.e("TAG", "chainingCall: ", e)
  }
}

Async with coroutineScope

val scope = CoroutineScope(Job() + exceptionHandler)

// Exception handled in the root
scope.launch {
   async { job2() }.await()
}

// Exception caught in the try/catch and also propagates up to the root.
scope.launch {
   try {
       async { job2() }.await()
   }catch (e : Exception){
       Log.e("TAG", "chainingCall: ", e)
   }
}

Async with supervisorScope

// exception handled in root
scope.launch {
  supervisorScope {
      async { job2() }.await()
  }
}

// Exception handled in try/catch
supervisorScope {
  try {
      async { job2() }.await()
  }catch (e : Exception){
      Log.e("TAG", "chainingCall: ", e)
  }
}

In the above code blocks you can see how and where the exceptions are caught with different scopes with launch and async builder.

Available coroutine scopes

  • GlobalScope (bound to the lifecycle of the application )
  • runBlocking ( will block the thread until its execution is completed).
  • viewModelScope ( bound to the lifecycle of the viewModel )
  • lifeCycleScope ( bound to the lifecycle of that class )

Many Jetpack libraries include extensions that provide full coroutines support. Some libraries also provide their own coroutine scope that you can use for structured concurrency.

Hope this blog helps you to understand the basics of coroutine and how we can make use of it in our application. Moving forward with our demo application we will see how to integrate coroutine with retrofit to make REST api calls.

Check out the other blogs to know about best practices in android Android best practice and check out the feature blogs Login feature and Product listing and detail to know how we can apply the best practices in our application. 

Leave a Reply