Injecting Coroutine Dispatcher with Dagger or Hilt
April 27, 2020
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.