Android Native - Inject Coroutine Dispatchers Into ViewModels Using Hilt

dimitrilc 1 Tallied Votes 901 Views Share

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:

  1. How to inject coroutine dispatchers into ViewModels.

Tools Required

  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1 Patch 1.

Prerequisite Knowledge

  1. Hilt basics.
  2. ViewModel basics.
  3. Intermediate Android.
  4. Coroutine basics.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add a new ViewModel called MyViewModel.

     class MyViewModel : ViewModel() {
    
     }
  3. Add the Hilt plugin into the project build.gradle.

     id 'com.google.dagger.hilt.android' version '2.44' apply false
  4. Add the following plugins into your module build.gradle.

     id 'kotlin-kapt'
     id 'com.google.dagger.hilt.android'
  5. 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'
  6. Add the Hilt application class into your project.

     @HiltAndroidApp
     class MyApplication : Application() {
     }
  7. Add the application class into the manifest, inside the <application> tag.

     android:name=".MyApplication"
  8. 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.

Screenshot_2022-11-10_at_1.05.40_PM.png

Screenshot_2022-11-10_at_1.07.25_PM.png

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.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.