Android Development, Kotlin

Structured concurrency with Kotlin coroutines

In a previous article, I tried to explain coroutines on Android in what I would call the easy way. In this article, I would try to explain coroutine scopes and how structured concurrency in Kotlin works.


Coroutine builders

Coroutine builders are functions that help us create a coroutine. They can be called from normal functions because they do not suspend themselves. Three coroutine builders are listed below:

  • launch: start a coroutine in the background and keep working
  • async: perform an asynchronous operation and return a deferred object which is the equivalent of a JavaScript Promise. We can call await on the deferred value in order to wait and get the result.
  • runBlocking: this blocks the current thread and waits for the coroutine to finish execution.


Launching Coroutines

Coroutines are launched in coroutine builders and are bound by a coroutine scope. A lazy way to launch a coroutine would be the use the GlobalScope. This means that the coroutine would be running for as long as the application is running and if there is no important reason to do this, I believe that it’s a way to wrongly use resources.

In launching coroutines, we would use the launch keyword like so:

    doSomething() // does some heavy computation in the background
    ... do other stuff

suspend fun doSomething(){
   // heavy computation here

However, as stated earlier, we don’t want to launch coroutines in the GlobalScope. When launching a coroutine, we want to be sure that we do it right so that we can avoid coroutine leaks & eventually running out of memory.

In my opinion, launching coroutines in GlobalScope and forgetting to keep a reference so that we can manually cancel later could be as error-prone as using callbacks.  How do we solve this problem? That’s where structured concurrency comes in.


Structured Concurrency

We can launch coroutines in the specific scope of the operation we are performing. On Android, you can use a scope to cancel all running coroutines when, for example, the user navigates away from an Activity or Fragment.

To start a coroutine, we would need to create the scope like so:

class MainViewModel : ViewModel(){
   private lateinit var job: Job
   private lateinit var coroutineScope: CoroutineScope
   private fun initialize(){
       // create job & scope
       job = Job()
       coroutineScope = CoroutineScope(Dispatchers.Main + job)

As seen above, When using scopes, we would be able to specify a dispatcher. A dispatcher controls which thread runs a coroutine. Some dispatchers include:

  • Dispatchers.Default – The default dispatcher used by coroutine builders (e.g. async, launch) if no dispatcher was specified
  • Dispatchers.Main – This dispatcher is confined to the main thread so if you’d like to update the UI thread, use this.
  • Dispatchers.IO – This dispatcher can be used when we want to do blocking IO tasks on another thread so that we don’t free or possibly crash the app.

In the code snippet above, the coroutineScope will be started in the main thread (we know this because we specified that the dispatcher should be Dispatchers.Main).

If you’re wondering what the point is and why we’d want to start a coroutine on the main thread, that’s fine. However, know that the coroutine will not block the main thread while it’s suspended. I explained how it would work here.

To cancel this coroutine, it’s a lot easier because we only have to do it once. We may want to cancel once the view model is no longer used (when the activity or fragment using the view model is no longer active).

override fun onCleared(){
   // anything else you'd like to do

Once we cancel job, all the coroutines launched by coroutineScope will be canceled as well. Canceling jobs help us prevent coroutine leaks.


Coroutine scopes

There are libraries that actually help us do structured concurrency better without having to write boilerplate in every ViewModel. In other to use these libraries, we need to know the types of coroutine scopes that exist.

ViewModel scope

Coroutines in this scope are useful when there is work that should only be done when a ViewModel is active. To avoid boilerplate, add the code below to your build.gradle file.

implementation “androidx.lifecycle:lifecycle-viewmodel-ktx:$view_model_scope_version”

This library adds viewModelScope as an extension function and binds the scope to Dispatchers.Main by default. However, the dispatcher can be changed if need be. The job is also automatically cancelled when the ViewModel is cleared so all we have to do is this:

class MainViewModel : ViewModel(){
   private fun doSomething(){
      viewModelScope.launch(Dispatchers.Default){ //Specify dispatcher if you like
         // Coroutine is launched. Time to do something.

   // No need to override onCleared, it's taken care of :)

Lifecycle scope

A LifecycleScope is defined for each Lifecycle object. LifecycleOwner could be an Activity or a Fragment. Any coroutine launched in this scope is canceled when the Lifecycle is destroyed. You can access the Lifecycle’s CoroutineScope either via lifecycle.coroutineScope or lifecycleOwner.lifecycleScope properties. To use this, add the following dependency to your build.gradle file

implementation “androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_scope_version”

Launching a coroutine using this scope can be done like this:

class HomeFragment: Fragment() {
   private fun doSomething(){
      lifecycleOwner.lifecycleScope.launch {
         // Coroutine launched. Do some computation here.

This is a lot cleaner as opposed to writing boilerplate when you want to launch a coroutine.


There’s more

I also talked about coroutine scopes at Google I/O Extended Ajah meetup. You can watch the video here :).



In this article, I tried to explain coroutine builders and structured concurrency. If you’d like to see more about structured concurrency, click here.


Spread the love

2 thoughts on “Structured concurrency with Kotlin coroutines

Leave a Reply

Your email address will not be published. Required fields are marked *