Kotlin Exploring Builders and Scopes - mariamaged/Java-Android-Kotlin GitHub Wiki

Kotlin - Exploring Builders and Scopes

Builders Build Routines

All coroutines start with a coroutine builder.

  • The block of code passed to the builder, along with anything called from that code block (directly or indirectly), represents the coroutine.
  • Anything else is either part of some other coroutine or is just ordinary application code.

So, you can think of a coroutine as a call tree and related state for those calls, rooted in the lamda expression supplied to the coroutine builder.

The Basic Builders

  • There are two coroutine builders that we will focus on:
    • launch().
    • async().
  • There are few others, though, that you will use in certain circumstances.

launch()

launch() is the "fire and forget" coroutine builder.

  • You pass it a lamda expression to form the root of the coroutine.
    • And you want that code to be executed.
    • But you are not looking to get a result directly back from that code.
  • Instead, that code only has side affects: updating other data structures within your application.
  • launch() returns a Job object, which we can use for managing the ongoing work, such as cancelling it.
import kotlinx.coroutines.*  
  
fun main() {  
    GlobalScope.launch(Dispatchers.Main) {  
	println("This is executed before the delay.")  
        stallForTime()  
        println("This is executed after the delay.")  
    }  
    println("This is executed immediately.")  
}  
private suspend fun stallForTime() {  
    withContext(Dispatchers.Default) {  
    delay(1000L)  
    }  
}

async()

  • async() also creates a coroutine and also returns a type of Job.
  • However, the specific type that async() returns is Deferred, a sub-type of Job.
  • async() also receives a lamda expression to serve as the root of the coroutine, and async() executes that lamda expression.
     
  • However, while launch() ignores whatever the lamda expression return, async() will deliver that to callers via the Deferred object.
  • You can call await() on a Deferred object to block until that lamda expression result is ready.
     
  • await() itself is a suspend function, so you need to call it inside of some other coroutine.
     
  • Here, we use async() to kick off some long calculation.
  • We then await() the result and use it.
import kotlinx.coroutines.*  
  
fun main() {  
    GlobalScope.launch(Dispatchers.Main) {  
	  val deferred = GlobalScope.async(Dispatchers.Default) {  
			delay(2000L)  
          println("This is executed after the delay.")  
          1337  
  }  
  
     println("This is executed after calling async().")  
     val result = deferred.await()  
     println("This is the result: $result")  
    }  
  println("This is executed immediately.")  
}
This is executed immediately 
This is executed after calling async() 
This is executed after the delay 
This is the result: 1337

Scope = Control

  • A coroutineScope, as emobied in a CoroutineScope implementation, exists to control coroutines.

A CoroutineScope keeps track of all your coroutines, and it can cancel all of the coroutines started in it.

  • In particular, in the current implementation of coroutines, a coroutine scope exists to offer "structured concurrency" across multiple coroutines.
  • In particular, if one coroutine in a scope crashes, all coroutines in the scope are cancelled.

Where Scopes Come From

GlobalScope

  • It is a singleton instance of a CoroutineScope, so it exists for the lifetime of your process.

In sample code, GlobalScope gets used a fair bit, because it is easy to access and is always around.

In general, you will not use it in production development.

Coroutine Builders

  • By default and convention, creating a Job creates an associated CoroutineScope for that job.
  • So, calling a coroutine builder creates a CoroutineScope.
  • In particular, the scope associated with a job is used when we nest coroutines.

Framework-supplied Scopes

  • A programming environment that you are using might have scopes as part of their API.
  • In particular, things in a programming environment that have a defined lifecycle and have an explicit "canceled" or "destroyed" concept might have a CoroutineScope to mirror that lifecycle.
     
  • For example, in Android app development, the androidx.lifecycle:lifecycle-viewmodel-ktx library adds a viewModelScope extension property to ViewModel.
    • ViewModel is a class whose instances are tied to some activity or fragment.
    • A ViewModel is "cleared" when that activity or fragment is destroyed for good, not counting any destroy-and-recreate cycles needed for configuration changes.
    • The viewModelScope is canceled when a ViewModel is cleared.
    • As a result, any coroutines created by coroutine builders (e.g., launch()) on a viewModelScope get cancelled when the ViewModel is cleared.

withContext()

The withContext() function literally creates a new CoroutineContext to govern the code supplied in the lamda expression.

  • The CoroutineScope is an element of a CoroutineContext, and withContext() creates a new CoroutineScope for its new CoroutineContext.
import kotlinx.coroutines.*  
  
fun main() {  
    GlobalScope.launch(Dispatchers.Main) {  
	println("This is executed before the delay.")  
        stallForTime()  
        println("This is executed after the delay.")  
    }  
    println("This is executed immediately.")  
}  
private suspend fun stallForTime() {  
    withContext(Dispatchers.Default) {  
    delay(1000L)  
    }  
}

GlobalScope is a CoroutineScope.

launch() creates another CoroutineScope.

withContext() creates yet another CoroutineScope.

They are all nested, to support "structured concurrency".

coroutineScope()

  • withContext() usually is used to changed the dispatcher, to have some code execute on some other thread pool.
     
  • If you want a new CoroutineScope for structured concurrency, you can use coroutineScope() and keep your current dispatcher.

supervisorScope()

  • The default behaviour of a CoroutineScope is if one coroutine fails with an exception, the scope cancels all coroutines in the scope.

Frequently, this is what we want.

  • If we are doing N coroutines.
  • And need the results of all N of them to proceed.
  • As soon as one crashes, we know that we do not need to waste the time of doing the rest of work.

However, sometimes, that is not what we want to do.

  • For example, suppose that we are uploading N images to a server.
  • Just because one image upload fails does not necessarily mean that we want to abandon uploading the remaining images.
  • Instead, we might want to complete the rest of the uploads, then find out about the failures and handle them in some way.
  • supervisorScope() is very similar to coroutineScope(), except that it skips the default failure rule.
  • The failure of one coroutine due to an exception has no impact on the other coroutines executed by this scope.
  • Instead there are ways that you can set up your own rule for how to deal with such failures.