Subbu Lakshmanan

Suspending functions should not be called on a different dispatcher

Use best practices to avoid using different dispatchers for suspending functions

Why

There seems to be confusion around using Dispatchers in Coroutines. Dispatchers are used to mention the thread on which the coroutine should run.

  • Dispatchers.Main - Main thread
  • Dispatchers.IO - IO thread
  • Dispatchers.Default - CPU-intensive thread

While we should mention the dispatcher for the coroutine based on the work a function does, We don't need to do the same for the caller of the suspending function. i.e., The suspend function that does the actual work handles what dispatcher to use, and the caller of the function should not worry about it.

In fact, various libraries explicitly provide suspending APIs for long-running blocking operations. The complexity of moving the appropriate tasks to background threads is already taken care of within the library. When calling the library's suspending API, it does not have to be considered.

P.S: Dispatchers.Unconfined is not recommended to be used in production code and is not discussed here.

What to do

  • When you write a long-running or CPU intensive task, wrap the code with the appropriate dispatcher so that the caller of the function need not worry about it.
  • If the library provides suspending APIs, the library will take care of the dispatchers. (It's a good practice to check the documentation of the library to see if it provides suspending APIs)
  • If you are calling an existing suspending function, check if it uses the appropriate dispatcher. If not, wrap it with appropriate dispatcher to avoid mentioning one in the caller of the function.

How to do

Here's an example:

fun onDoneClicked() {
    viewModelScope.launch(Dispatcher.IO) {
        initiateSeeding()
    }
}

...

suspend fun initiateSeeding() {
    async {
        // Do some work that runs in IO thread
    }
}
  1. Wrap the long-running IO task with Dispatcher.IO

    
    suspend fun initiateSeeding() {
        async(Dispatcher.IO) {
            // Do some work that runs in IO thread
        }
    }
    
  2. Remove the dispatcher from the caller of the function

    fun onDoneClicked() {
        viewModelScope.launch {
            initiateSeeding()
        }
    }
    

Another good example is Retrofit. Retrofit provides suspending APIs for network calls. The library takes care of the dispatchers, and the caller of the function need not worry about it.

interface SeedApiService {
    @GET("seeds")
    suspend fun getSeeds(): List<Seed>
}

class SeedRepository(private val seedApiService: SeedApiService) {
    suspend fun getSeeds(): List<Seed> = seedApiService.getSeeds()
}

class SeedViewModel(private val seedRepository: SeedRepository) {
    fun fetchSeeds() {
        viewModelScope.launch {
          try {
            val seeds = seedRepository.getSeeds()
            // Do something with the seeds
          } catch (e: Exception) {
            // Handle error
          }
        }
    }
}

The library will handle the dispatchers, and We don't need to mention it in the viewModelScope.launch function.

References


All rights reserved