How to Implement Hilt in Android App?

Dependency injection library is hard to use, this article provides the simple and easy to follow steps to Implement Hilt in your Android app.

ยท

5 min read

How to Implement Hilt in Android App?

Introduction

There are 3 type of dependency injections:

  • Manual dependency injection - Inject dependency through constructor parameters
  • Service locator - Singleton container that holds the dependencies
  • Dependency injection library - Library/framework such as Hilt/Dagger and Koin provide similar functionality as manual dependency injection with less code.

Dependency injection library is hard to use. When I first heard about Hilt/Dagger and Koin, I had no idea what they are about. So I read their documentations and tutorials, it makes my understanding even worst!

My first experience was Koin in my Android Kotlin Developer Nonodegree program. The code is already provided, and I used it without 100% understanding it.

Instead of trying to understand Koin, I came across Hilt which is built on top of Dagger, and it supposes to be a user-friendly. So I gave this a try and I found this useful and simple official tutorial - Using Hilt in your Android app. This tutorial shows you step-by-step to convert service locator into Hilt dependency injection.

So I think it maybe a good idea for me to document the steps to implement Hilt. This article summarizes the steps in this Hilt tutorial.

1. Setup Hilt Dependencies

In build.gradle(project level), add Hilt Android gradle plugin.

buildscript {
    ...
    }
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40'
    }
}

In build.gradle (app level), add kotlin-kapt and dagger.hilt.android.plugin.

plugins {
    ...
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

Add Hilt implementation dependencies.

dependencies {
    ...
    implementation 'com.google.dagger:hilt-android:2.40'
    kapt 'com.google.dagger:hilt-android-compiler:2.40'
   ...
}

2. Add @HiltAndroidApp in your application class

If you don't have an application class, you need to create one.

@HiltAndroidApp
class LogApplication : Application() {
    ...
}

Also, you need to update android:name in the AndroidManifest.xml.

<manifest>

    <application
        android:name=".LogApplication"
        ...
    </application>
</manifest>

3. Add @AndroidEntryPoint in your activity and fragment

If you want to inject your dependencies in activity, you only need to add @AndroidEntryPoint in your activity class. However, if you want to inject your dependencies in fragment, you need to add @AndroidEntryPoint in both fragment and activity that hosts the fragment.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    ...
}

@AndroidEntryPoint
class LogsFragment : Fragment() {
    ...
}

4. Add @Inject lateinit var to perform field injection

Use @Inject lateinit var on the class field where you want Hilt automatically create the instance for you. In this example, Hilt will automatically create the logger instance for you after the fragment is first attached (i.e. after onAttach() is called)

class LogsFragment : Fragment() {
    ...
    @Inject lateinit var dateFormatter: DateFormatter
    @Inject lateinit var logger: LoggerDataSource

    ...
}

5. Add @Inject constructor() to tell Hilt how to provide dependencies

Hilt doesn't know how to create the DateFomatter and LoggerDataSource. To do that, you need to add @Inject constructor() into the classes.

//Hilt knows how DataFormatter can be constructor injected
class DateFormatter @Inject constructor() {
    ...
}

//Hilt still doesn't know how LoggerLocalDataSource can be constructor injected 
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
    ...
}

Due to missing information on LogDao, Hilt still doesn't know how to constructor inject into LoggerLocalDataSource. Thus, we need to create the hilt module - step 7 below to tell how LogDao can be created.

6 Add @Singleton to scope instance to entire application

@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) : LoggerDataSource {
    ...
}

This means the same LoggerLocalDataSource will be used for the entire application. There are different component scopes such as @ActivityScoped, FragmentScoped and @ViewModelScoped.

7. Add @Module and @InstallIn to create Hilt Modules

For those class that Hilt doesn't know how to constructor inject the dependencies, you need to create the Hilt Module.

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

@InstallIn specify which Hilt component this module should be installed into which is related to the component scopes.

8. Inject instances with @Provides

There are 2 @Provides. The first @Provides provides the information how LogDao can be created. Because it depends on AppDatabase, the second @Provides provides information how AppDatabase can be created.

@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {

    @Provides
    fun provideLogDao(database: AppDatabase): LogDao {
        return database.logDao()
    }

    @Provides
    fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
        return Room.databaseBuilder(
            appContext,
            AppDatabase::class.java,
            "logging.db"
        ).build()
    }
}

Please note the @ApplicationContext is the pre-defined binding which means Hilt will automatically get the ApplicationContext for you.

9. Inject interface instances with @Binds

If the injected field is an interface, you need to tell Hilt how to provide the implementation of the interface using @Binds.

@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {

    @Binds
    abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}

Please note that the abstract function and class are needed with @Binds.

10. Add @Qualifier for two implementations that have the same interface

If your field injection is an interface, and you have more than one implementation, you need to use @Qualifier to tell Hilt how to differentiate them.

Simple_Steps_to_Implement_Hilt_in_Android_App_01.png

For example, you need to add @Qualifier for LoggerLocalDataSource and LoggerInMemoryDataSource.

Define the @Qualifier class in the Hilt modules that binds the LoggerLocalDataSource and LoggerInMemoryDataSource.

@Qualifier
annotation class InMemoryLogger

@Qualifier
annotation class DatabaseLogger

Add @DataBaseLogger and @InMemoryLogger qualifiers above the Binds.

@InstallIn(SingletonComponent::class)
@Module
abstract class LoggingDatabaseModule {

    @DatabaseLogger
    @Binds
    abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}

@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {

    @InMemoryLogger
    @Binds
    abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}

Add a qualifier above the field injection variable to indicate which implementation to use. For example, @DatabaseLogger indicates LoggerLocalDataSource implementation will be used.

@DatabaseLogger
@Inject lateinit var logger: LoggerDataSource

Conclusion

Honestly, I'm not a fan of dependency injection library (for now). It makes the code harder to understand. If there is a compilation error, it is harder to fix. At least for me, it is hard. For now, it doesn't improve my productivity, but the other way round.

On the other hand, service locator technically is not dependency injection because it doesn't inject the dependencies. It allows the consumers to retrieve the dependencies instead from everywhere. It is basically a global object.

Given all these reasons, I still prefer to use manual or constructor dependency injection. It is simple, clear, and easy to debug. Anyone can understand your code. Isn't this all clean code about?

I know I say this because I haven't mastered dependency injection. I hope one day I will.

Did you find this article valuable?

Support Vincent Tsen by becoming a sponsor. Any amount is appreciated!

ย