Basic knowledge about Coroutine Scope and Dispatchers

coroutine-scope-and-dispatchers

Hello, this is the 2nd article in the Kotlin Coroutine tutorial series. In this article we will learn about Coroutine Scope

What is Coroutine Scope?

As we know in the previous article, to start a coroutine we need to use the launch or async function. These are essentially extension functions of CoroutineScope. Or in other words, every time we want to start a coroutine, we need a Coroutine Scope to start.
Coroutine scope is what creates the environment we use to start coroutines. It also manages the lifecycle of coroutines. There are a number of Coroutine Scopes provided by the kotlinx.coroutines or androidx.lifecycle libraries. We can also create our own Custom Coroutine scope to use.
When a Coroutine scope is destroyed, the coroutines it started previously will also be destroyed, if they are still active.

Global Scope

One of the easiest ways to start a Coroutine is to use GlobalScope. This is a scope that exists along with the life of the application, in other words, it is never destroyed while the application is running. Only when the application stops is the Global Scope destroyed.
We have the following example:

GlobalScope.launch {
  Log.d("Coroutine", "Started by Global Scope")
  delay(2000)
  Log.d("Coroutine", "Completed")
}

When we run this code we will have the following output:

Started by Global Scope
//After 2000ms
Completed

But what if we kill the application before 2000ms has elapsed? We will not see the “Completed” log line, because then the GlobalScope has been destroyed, so the Coroutine that it started is also canceled.

LifecycleScope

This is a coroutine scope provided by the Android Jetpack library. It is available inside activities and fragments. This Coroutine’s lifecycle is anchored to the lifecycle of the Activity and Fragment containing it. In other words, when an activity or fragment is destroyed, LifecycleScope is also destroyed. And of course, then the coroutines that it starts are also canceled.

We have an example of using lifecycleScope with Activity

lifecycleScope.launch {
  Log.d("Coroutine", "Started by lifecycle scope")
  delay(2000)
  Log.d("Coroutine", "Completed")
}

Similar to when using Global Scope, when running this code we will have output
When we run this code we will have the following output:

Started by Global Scope
//After 2000ms
Completed

But if we press the back button before 2000ms has passed. We will not see the “Completed” log line, because then LifecycleScope has been destroyed.
Lifecycle is useful when we want to make sure that after the activity or fragment is destroyed, the coroutines are destroyed as well. What if you display a dialog or a toast message in an activity after it has been destroyed? We obviously don’t want that, right? The handling will be similar to the following:

lifecycleScope.launch (Dispatchers.IO) {
  //Simulate long running task
  delay(3000)
  //After getting the result
  withContext(Dispatchers.Main) {
    Toast.makeText(this@MainActivity2, "Completed", Toast.LENGTH_SHORT).show()
  }
}

ViewModelScope

Similar to LifecycleScope, ViewModelScope is provided by the Android Jetpack library. The ViewmodelScope lifecycle depends on the lifecycle of the ViewModel that contains it. In Android projects, we often want to separate business logic and UI. Therefore, logical processing often takes place in ViewModels. So we need to make sure that the lifecycle of the coroutines used in the Viewmodel are managed safely and properly. ViewModelScope was created to do just that. When Viewmodel is cleared, ViewmodelScope will also be destroyed, thereby safely destroying coroutines.

runBlocking

Có 1 cách khác để tạo ra 1 coroutine scope mà không cần chỉ rõ ra Coroutine Scope, đó là sử dụng phương thức runBlocking.Phương thức này sẽ block thread hoặc Coroutine tạo ra nó cho đến khi nó hoàn tất công việc của mình. Ta có ví dụ như sau:

GlobalScope.launch {
  runBlocking {
    Utils.log("Using runblocking")
    delay(400)
  }
  launch {
    Utils.log("After runblocking")
  }
}

The result will be

Using runblocking
//After 400ms
After runblocking

As you can see, the runBlocking method will block the coroutine created by GlobalScope.launch until it completes.
Note: In a coroutine scope, we should not use runBlocking because it will block that coroutine scope. runBlocking is designed to use suspend functions without needing to initialize coroutine scope. However, it will block the thread on which we call it.
Another application of runBlocking is to write unit tests.

Dispatchers

Coroutines help us run code in a parallel and asynchronous way. And of course they help us manage the threads on which the code will run. We will learn how to assign threads to a coroutine through Dispatchers.

Because this part is related to threading, I will write a Utils to help us log messages with thread information.

object Utils {
  fun log(message: String) {
    Log.d("Coroutine", "[thread info: ${Thread.currentThread().name}] $message")
  }
}

Dispatcher.Main

This is the Dispatcher indicating that the coroutine will use the Main thread. A common use case is that after you have received data from an api call (of course in the background), you need to update the UI for the application and you must use the Main thread. Dispatchers.Main helps us do this:

GlobalScope.launch (Dispatchers.Main) {
  Utils.log("Dispatcher Main")
}

Result:

[thread info: main] Dispatcher Main

You can see the coroutine uses Main thread to handle the task.

Dispatchers.IO

This is the Dispatcher indicating that the coroutine will be used to handle IO tasks, such as reading and writing files, requesting API calls. Coroutines use a shared thread pool to create background threads for these tasks. For example:

GlobalScope.launch (Dispatchers.IO) {
  Utils.log("Dispatcher IO")
}

Result:

[thread info: DefaultDispatcher-worker-1] Dispatcher IO

You can see that the coroutine uses a thread named DefaultDispatcher-worker-1 to process the task.

Dispatchers.Default

We can use this Dispatcher to perform heavy tasks that consume CPU performance.
There will be a separate article to compare the difference between Dispatcher Default and IO

Dispatchers.Unconfined

This doesn’t change the thread and launches the coroutine in the caller thread. The important thing here is that after suspension, it resumes the coroutine in the thread that was determined by the suspending function.

Leave a Reply

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