Introduction
When working with ViewModels, instead of using the default, it is best practice that we inject coroutine dispatchers instead of hard-coding them. The reasoning behind this is that it is easier to switch to a testable dispatcher in tests.
In this tutorial, we will learn how to inject coroutine Dispatchers using Hilt.
Goals
At the end of the tutorial, you would have learned:
- How to inject coroutine dispatchers into ViewModels.
Tools Required
- Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1 Patch 1.
Prerequisite Knowledge
- Hilt basics.
- ViewModel basics.
- Intermediate Android.
- Coroutine basics.
Project Setup
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Add a new ViewModel called MyViewModel.
class MyViewModel : ViewModel() { }
-
Add the Hilt plugin into the project build.gradle.
id 'com.google.dagger.hilt.android' version '2.44' apply false
-
Add the following plugins into your module build.gradle.
id 'kotlin-kapt' id 'com.google.dagger.hilt.android'
-
Add the dependencies below into your module build.gradle.
implementation "com.google.dagger:hilt-android:2.44" kapt "com.google.dagger:hilt-compiler:2.44" implementation 'androidx.activity:activity-ktx:1.5.0'
-
Add the Hilt application class into your project.
@HiltAndroidApp class MyApplication : Application() { }
-
Add the application class into the manifest, inside the
<application>
tag.android:name=".MyApplication"
-
In MainActivity, add the property below.
val myViewModel: MyViewModel by viewModels()
Avoid Hard-Coding Coroutine Dispatchers In ViewModels
Let us look at some hard-coding examples. Add the init block and function below to your MyViewModel.
// Hard-coding example
init {
// ViewModelScope already uses Dispatchers.Main by default
viewModelScope.launch(Dispatchers.Default) {
}
}
// Hard-coding example
private fun hardCodingTest(){
viewModelScope.launch(Dispatchers.IO){
// Do IO work
}
}
As we can see, the coroutine dispatchers IO
and Default
are hard-coded here, which will make it hard to switch to a testable coroutine dispatcher later on.
If you have SonarLint installed in Android Studio, you will see the pattern highlighted as well.
Inject Coroutine Dispatchers Into ViewModel
We already have Hilt setup, so all we have to do next is to add a Hilt module to provide the dispatchers. We will use Hilt modules because the dispatchers IO
, Default
, and Main
are instances.
Declare the object below in your project. This will allow us inject the IO
coroutine dispatcher.
@Module
@InstallIn(SingletonComponent::class)
object CoroutineDispatchersModule {
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
}
To inject this IO
dispatcher and use it in code, we can modify our MyViewModel like below.
class MyViewModel @Inject constructor(
private val ioDispatcher: CoroutineDispatcher
) : ViewModel() {
// injected example
init {
viewModelScope.launch(ioDispatcher){
}
}
Inject More Coroutine Dispatchers
The CoroutineDispatchersModule can be modified to allow for injecting other dispatchers.
@Module
@InstallIn(SingletonComponent::class)
object CoroutineDispatchersModule {
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}
There is a small problem here. All three provide functions return the same type of CoroutineDispatcher
. Fortunately, you can define custom qualifiers to assist Hilt in deciding which instance to inject.
@Module
@InstallIn(SingletonComponent::class)
object CoroutineDispatchersModule {
@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@MainDispatcher
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class IoDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DefaultDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainDispatcher
Finally, you can use these dispatchers in the same ViewModel like below.
class MyViewModel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
@MainDispatcher private val mainDispatcher: CoroutineDispatcher
) : ViewModel() {
Summary
Congratulations, we have learned how to inject coroutine dispatchers into ViewModel in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebInjectDispatchersViewModel.