Hilt
Last modified on Mon 22 Mar 2021

Hilt

Hilt is a tool that helps you set up Dagger fast and easy in your app. After placing a series of annotations in the right places, Hilt will do all the work for you.

This includes:

Setup

First, add the hilt-android-gradle-plugin plugin to your project's root build.gradle file:

buildscript {
    ...
    ext.hilt_version = '2.33-beta'
    dependencies {
        ...
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

Then, apply the Gradle plugin and add these dependencies in your app/build.gradle file:

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

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

Hilt uses Java 8 features. To enable Java 8 in your project, add the following to the app/build.gradle file:

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

Annotations

Let's start by listing the most important annotations. Later we will expand by explaining how each of them works.

How it works

In Hilt, everything happens through annotations. As I mentioned before, the first one you need to add is @HiltAndroidApp above your application class. This will generate all the components that you will need.

Keep note of the scope of each generated component. For example, if some dependency is used in ViewModels only, you need to install it in the ViewModelComponent.

@AndroidEntryPoint is the next annotation that you will probably use the most. Placed above an Activity or Fragment, it makes them viable for injection by adding them to Hilt generated builder modules. No further action required.

@HiltViewModel does the same, just for view models. Once you add this annotation, you can feel free to @Inject dependencies to your view models. As long as dependencies are provided in some @Module.

@Module and @InstallIn are the pair you will want to use with your modules. Every module consist of either @Provides or @Binds annotated functions that will provide dependencies as explained earlier.

@Inject placed beside a dependency in the constructor, field or method will make sure that your dependency is injected where needed.

Let's dive into a real project example to see how this all works together.

Example

First we start with an application class:

@HiltAndroidApp
class AliasApp : Application()

Then, define some modules that will provide dependencies:

@Module
@InstallIn(SingletonComponent::class)
class RemoteConfigModule {

    @Provides
    @Singleton
    fun remoteConfig(): FirebaseRemoteConfig =
        Firebase.remoteConfig
}
@Module
@InstallIn(ViewModelComponent::class)
interface RepositoriesModule {

    @Binds
    fun teamsRepo(teamsRepository: TeamRepositoryImpl): TeamRepository

    @Binds
    fun wordsRepo(wordsRepositoryImpl: WordRepositoryImpl): WordRepository

    @Binds
    fun myWordsRepo(wordsRepositoryImpl: MyWordRepositoryImpl): MyWordRepository

    @Binds
    fun databaseVersionRepo(databaseVersionRepositoryImpl: DatabaseVersionRepositoryImpl): DatabaseVersionRepository
}

We are using the SingletonComponent for the first module, because we need remote config in various places, so we are making sure it's available in the global scope. Also, by using the @Singleton annotation, we are making sure that a single instance will be made and provided in all places.

In the second module, we are using the @Binds annotation to bind repository interfaces to their implementations. The listed repositories are only used in ViewModels so we are installing them in the ViewModelComponent.

Let's set up our ViewModel:

@HiltViewModel
class GameViewModel @Inject constructor(
    private val wordRepository: WordRepository,
) : ViewModel() {

   ...

   // Example of usage
   fun onSubmitWord(word: Word) {
       wordRepository.addWord(word)
   }
   ...
}

Having placed the @HiltViewModel above our ViewModel, we can now inject dependencies that are in either SingletonComponent or ViewModelComponent by using the @Inject annotation on the constructor or above fields or methods.

In this case, we are injecting the constructor with an instance of WordRepository.

Now that we have everything in place, let's jump to an activity:

@AndroidEntryPoint
class GameActivity : AppCompatActivity() {

    ...
    private val viewModel by viewModels<GameViewModel>()

    ...

    // Example of usage
    fun onSubmitWord(word: Word) {
        viewModel.onSubmitWord(word)
    }

Kotlin and Hilt will provide you with a neat feature that enable you to use Kotlin delegates when inject ViewModels.

Hilt makes the built-in delegate viewModels use the injected version of the ViewModel.

For other types of dependencies, you might have to use @Inject above a field directly.

This covers most of the typical use-cases of Hilt that you might encounter. However, Hilt has many more different use cases. For more information see the official guide.