Injecting Coroutine Dispatcher with Dagger or Hilt

April 27, 2020

rustic.JPG

When you work with Coroutines and Flow, you’d want to specify what Dispatcher the work will be performed on. Here is a list of CoroutineDispatchers and they all have different purpose:

  • Dispatchers.Default

  • Dispatchers.IO

  • Dispatchers.Main

  • Dispatchers.Unconfined

The reason you’d want these Dispatchers to be injected in a function is to allow overwriting it by a test.

In other words, you don’t want to hardcode a Dispatcher in the flowOn:

class UserReposRepository @Inject constructor(
    private val apiService: ApiService,
) {

    suspend fun getUserRepos(login: String): Flow<Repo> {
        return apiService.getUserRepos(login).asFlow()
            .flowOn(Dispatchers.IO)
    }
}

You do want a way to inject it in the function instead doing something like this:

class UserReposRepository @Inject constructor(
    private val apiService: ApiService,
    @IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {

    suspend fun getUserRepos(login: String): Flow<Repo> {
        return apiService.getUserRepos(login).asFlow()
            .flowOn(ioDispatcher)
    }
}

Or, if you need multiple Dispatchers in your function:

class UserReposRepository @Inject constructor(
    private val apiService: ApiService,
    @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
    @IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {

    suspend fun getUserRepos(login: String): Flow<Repo> {
        // TODO use default dispatcher
        
        return apiService.getUserRepos(login).asFlow()
            .flowOn(ioDispatcher)
    }
}

We will talk about testing it at the end of the post

class UserReposRepository @Inject constructor(
    private val apiService: ApiService,
    @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
    @IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {

    suspend fun getUserRepos(login: String): Flow<Repo> {
        // do something on the default dispatcher
        // and then use the io dispatcher
        return apiService.getUserRepos(login).asFlow()
            .flowOn(ioDispatcher)
    }
}

Injecting CoroutineDispatcher

Since all of our dispatchers share the same type, CoroutineDispatcher, we need to help Dagger to distinguish between them and @Qualifier is an ideal Dagger construct for that.

Dispatchers Dagger module

Define each of the Dispatcher in the Dagger module using a @Qualifier annotation:

import dagger.Module
import dagger.Provides
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import javax.inject.Qualifier

@Module
object DispatcherModule {
    @DefaultDispatcher
    @Provides
    fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default

    @IoDispatcher
    @Provides
    fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

    @MainDispatcher
    @Provides
    fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DefaultDispatcher

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class IoDispatcher

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainDispatcher

And reference DispatcherModule in your AppComponent:

@Singleton
@Component(
    modules = [
        NetworkModule::class,
        DispatcherModule::class,
        ViewModelModule::class
    ]
)
interface AppComponent {
  ...
}

Ready to inject

And that’s it! Now you are ready to inject your Dispatchers whenever needed. For instance, let’s inject a Dispatchers.IO here:

class UserReposRepository @Inject constructor(
    private val apiService: ApiService,
    @IoDispatcher private val ioDispatcher: CoroutineDispatcher
)   

CoroutineDispatchers and Unit Testing

The main reason for us to inject CoroutineDispatchers is to have better control and be able to overwrite them in tests.

Testing ViewModels/UI

When using Coroutines in the UI layer, you are dispatching results to the Main (UI) thread.

In order to do so in unit tests, we need to override the Main Dispatcher with a test one, TestCoroutineDispatcher, like so:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description

class CoroutineTestRule(
    val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {
    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(dispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.resetMain()
        dispatcher.cleanupTestCoroutines()
    }
}

And using it in your UI tests is simple:

class UserReposViewModelTest {
  @get:Rule
  val rule = CoroutineTestRule()
    
  ...
}

What about testing other layers such as a repository above?

Again, we need to use a TestCoroutineDispatcher and pass it in any time we require a CoroutineDispatcher

class UserRepositoryTest {
  private val testDispatcher = TestCoroutineDispatcher()
  ...

  @Test
  fun `should get user details on success`() = runBlocking {
      val repository = UserRepository(apiService, testDispatcher)
      ...
  }

  @Test
  fun `should retry and all retries failed`() = testDispatcher.runBlockingTest {
    val repository = UserRepository(apiService, testDispatcher)
    ...
  }

Note that we use runBlocking and runBlockingTest above. The latter is necessary when your main code contains some time delays and we don’t want to wait for that delay in the tests. This gives us better control over test execution and, in particular, advancing time using advanceTimeBy

Bonus: RxJava

The same pattern can be applied to injecting Schedulers in RxJava as well.

Source code

The complete source code for the example above and much more can be found in FlowChannels101, repository I created with examples of using Flow and Channels and unit testing them. This was inspired by a talk presented by Mohit.

 
Previous
Previous

Binding ViewModels with non-empty constructors

Next
Next

Theming: Default Styles on Android