Dependency Injection with Hilt and RoomDB Callbacks Android

Dagger dependency injection has always been a pain point of Android Development - there wasn't a clear best practice for how to organise modules and weird annotations tried to make it more suitable for the Android platform.

Then comes Hilt. Dependency Injection is now easy and I believe anyone can pick it up in under 15 minutes. In this post I will walk through the steps I used to implement Hilt in an Android Application which uses Room and other Jetpack libraries.

Getting started

Just as is explained on the hilt guide we need to add the Gradle plugin and dependencies to our gradle files. It's important to note that the support for lifecycle and ViewModel components are in a separate dependency, so if you use these in your app, your app level build.gradle should look like this:
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
...
dependencies {
    ....
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
    kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}
Follow the rest of the instructions to setup the Gradle plugin.

Database

My database class used to manage its own instance,  however, with Hilt we don't need any code inside the class - this will be in a new AppModule class.

The AppModule should be annotated with @Module and @InstallIn to describe the scope of the components that it will provide. I run code on creation of the database and one of the answers in this StackOverflow post gave me a good way of doing this with Dagger. 
@InstallIn(ApplicationComponent::class)
@Module
object AppModule {

    lateinit var database: MyDatabase

    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): MyDatabase {
        // Make sure a read is made before writing so our onCreate callback is executed first
        database =  Room.databaseBuilder(
            context,
            MyDatabase::class.java, "database.db"
        )
            .addCallback(object : RoomDatabase.Callback() {
                override fun onCreate(db: SupportSQLiteDatabase) {
                    super.onCreate(db)
                    GlobalScope.launch {
                        MyDatabase.onCreate(database, context) // in companion of MyDatabase
                    }

                }
            })
            .build()
        return database
    }

    @Provides
    fun provideObjectDao(database: MyDatabase): ObjectDao {
        return database.objectDao()
    }

}
Since we don't have access to the Room database object in our callback, we need to rely on the fact that the callback will be called after .build() returns our database object. A field in AppModule does the trick. Just like Dagger, annotate your provides methods with @Providers and @Singleton if required. 

Also, notice the @ApplicationContext annotation. This is from Hilt, and means we don't need to create a provider for context. 

Simple injection

For classes that can simply be created with their constructor, you don't need to create a module and @Provides method. Just add @Inject before the constructor of your class like this
class ResourceManager @Inject constructor(@ApplicationContext applicationContext: Context) {
  // we can use the application context in init or field initialisation
}

Using these objects

For every Android specific component that will need injecting, we will use the annotation @AndroidEntryPoint. For example, in your activities and their fragments. Even if there is no injection in the Activity, if you require injection in a fragment your Activity must be annotated.
Now inside your classes you can get an instance of an object like this
@AndroidEntryPoint
class MyFragment : Fragment() {

    private val viewModel: MyViewModel by viewModels()

    @Inject lateinit var database: MyDatabase
   ...
}
But how is our viewModel going to be injected? Do we need a ViewModelFactory like before? Well it turns out that Hilt makes this just as easy for us. We need to annotate the constructor of the ViewModel with @ViewModelInject and that's all. 
class MyViewModel @ViewModelInject constructor(private val database: MyDatabase) : ViewModel() {
   ...
}
This requires the extra dependency I mentioned earlier.

Conclusion

I'm really impressed at how easy Hilt has made dependency injection on Android and would highly recommend you check it out! Let me know if you have any questions or suggestions. Thank you for reading :)

Comments