Dependency injection library is hard to use, this article provides the simple and easy to follow steps to Implement Hilt in your 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.
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.
Originally published at https://vtsen.hashnode.dev.
Top comments (0)