Dependency Injection with Hilt

Hilt is a new Dagger-based library for Android apps, providing an easy way to incorporate dependency injection with less boilerplate code. In this article, we’ll discuss how to use it.

Setup

First, add the Hilt Gradle plugin to the project’s root level build.gradle file:

buildscript {
    dependencies {
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
        ...
    }
    ...
}

Next, update the module’s build.gradle file:

apply plugin: "dagger.hilt.android.plugin"

dependencies {
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
    ...
}

...

As of the writing, the latest version is 2.28.3-alpha.

Annotate the Application

The app must have an Application class annotated with @HiltAndroidApp, which kicks off the code generation.

Assume this is what we have with Dagger:

class App : BaseApp(), HasAndroidInjector {
    private lateinit var appComponent: AppComponent

    @Inject
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>

    override fun onCreate() {
        super.onCreate()

        appComponent = DaggerAppComponent.builder().appModule(AppModule(this)).build()
        appComponent.inject(this)
    }

    override fun androidInjector(): AndroidInjector<Any> = dispatchingAndroidInjector
}

Now it can be simplified as:

@HiltAndroidApp
class App : BaseApp()

Annotate Activities / Fragments / etc.

Instead of calling AndroidInjection.inject(this) in the activity’s onCreate() method, we need to annotate it with @AndroidEntryPoint, e.g.:

@AndroidEntryPoint
class SettingsActivity : BaseActivity() {
    @Inject
    lateinit var settingsViewModel: SettingsViewModel

    ...
}

In addition to activities, we can also use the @AndroidEntryPoint annotation with fragments (the one from AndroidX), views, services, and broadcast receivers.

Hilt Modules

Hilt modules are standard Dagger modules that must be annotated with @InstallIn. This annotation declares the components that the module should be included in, e.g.:

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
   ...
}

Note that we no longer need to pass a reference to the Application to the AppModule. Hilt does that for us!

Components

Unlike Dagger, usually we don’t need to define components directly. Instead, Hilt provides a set of predefined components with corresponding scope annotations.

For example, the SingletonComponent has the same lifetime as an Android Application. It is created when Application.onCreate() is called.

Similarly, an ActivityComponent instance is created when the corresponding Activity’s onCreate() is called, and destroyed when onDestroy() is called. As expected, Hilt provides the access to the Application and also the corresponding Activity. However, we need to manually convert it to the specific activity type if needed, e.g.:

@Module
@InstallIn(ActivityComponent::class)
object SettingsActivityModule {
    @Provides
    fun provideSettingsActivity(activity: Activity): SettingsActivity
            = activity as SettingsActivity

    ...
}

Scoping

By default, all bindings are unscoped, meaning a new instance will be created each time requested. We can also scope a binding so that it will only be created once per component instance, e.g.:

@Module
@InstallIn(ActivityComponent::class)
object SettingsActivityModule {
    @ActivityScoped
    @Provides
    fun provideSettingsPresenter() = SettingsPresenter()

    ...
}

Note that the scope must match its corresponding component, e.g. we can only add the @ActivityScoped annotation to modules installed to ActivityComponent.

Conclusion

Hopefully, this article gives enough information for you to get started with Hilt. In case you’re interested, here’s a pull request to migrate to Hilt from Dagger in my toy project.


See also