Use Hilt in Android
If you want to use dagger in your project, you may have 2 or 3 different sources, different samples, different blog posts. All of them use a different set-up, and all of them use Dagger in a different way, so it’s very difficult to understand all the topics and relate them together. So that’s why Google create a common guidance, a common ground, which is called Hilt.
Hilt is an opinionated dependency injection library for Android that reduces the boilerplate of using manual DI in your project. Doing manual dependency injection requires constructing every class and its denpendencies by hand and using containers to reuse and manage dependencies.
Dependencies Container
Annotating Android classes with @AndroidEntryPoint
creates a dependencies container that follows the Android lifecycle.
@AndroidEntryPoint
class MainFragment: Fragment() {
...
}
Hilt currently supports the following Android types: Application
by using(@HiltAndroidApp
), Activity, Fragment, Service and BroadcastReceiver.
Hilt only supports activities that extend FragmentActivity and fragments that extend Jetpack library [Fragment][fx](Note:Hilt does not support retained fragments).
Container
A container is a class which is in charge of providing dependencies in your codebase and knows how to create instances of other types of your app. It manages the graph of dependencies required to provide those instances by creating them and managing their lifecycle.
A container exposes methods to get instances of the types it provides. Those methods can always return a different instance or the same instance. If the method always provides the same instance, we say that the type is scoped to the container.
@HiltAndroidApp
Annotating the Application class with @HiltAndroidApp
, we add a container that is attached to the app’s lifecycle:
@HiltAndroidApp
class App:Application() {
...
}
@HiltAndroidApp
triggers Hilt’s code generation including a base class for our application that can use dependency injection. The application container is the parent container of the app, which means that other containers can access the dependencies that it provides.
Provide dependencies
Constructor Inject
To tell Hilt how to provide instance of type, add the @Inject
annotation to the constructor of the class we want to be injected.
class Logger @Inject constructor() {
...
}
Scoping instance
Hilt can produce different containers that have different lifecycles, there are different annotations that scope to those containers:
@Singleton
: Application container@ActivityScoped
: Activity container@FragmentScoped
: Fragment container@ViewScoped
: View container
So we could annotating Logger.
@Singleton
class Logger @Inject constructor() {
...
}
Modules
For types that cannot be constructor injected, such as interface or classes are not contained in our project, we should use Hilt modules. An exemple of this is OkHttpClient
, we need to use its builder to create an instance.
A Hilt module is a class annotated with @Module
and @InstallIn
. @Module
tells Hilt this is a module and @InstallIn
tells Hilt the bindings are specifying in which Hilt component.
For each Android class that can be injected by Hilt, there’s an associated Hilt Component.
@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {
...
}
Providing instances with @Provides
We can annotate a function with @Provides
in Hilt modules to tell Hilt how to provide types that cannot be constructor injected.
The function body of the @Provides
annotated function will be executed every time Hilt needs to provide an instance of that type. The return type of the @Provides
annotated function tells Hilt the bindings type or how to provide instances of that type.
@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {
/**
* We always want Hilt provide the same database instance, so annotate this
* method with @Singleton.
*
* @param appContext: Each Hilt container comes with a set of default bindings that can be
* injected as dependencies into our custom bindings, to access this appContext, we annotate
* the filed with @ApplicationContext.
*/
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"logging.db"
).build()
}
/**
* provide AppDatabase dependency.
*/
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
Providing interface impl with @Binds
To tell Hilt what implementation to use for an interface, we can use the @Binds
annotation on a function inside a Hilt module.
@Binds
must annotate an abstract function, since Hilt modules cannot contain both non-static and abstract binding methods, so we cannot place @Binds
and @Provides
annotations in the same class.
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
/**
* To tell Hilt how to provide instances of AppNavigatorImpl, we just annotate its constructor with `@Inject`
*/
@Binds
abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}
Providing different interface impl with @Qualifier
Assume that LoggerDataSource interface has two different implementation.
interface LoggerDataSource {
...
}
@Singleton
class LoggerLocalDataSource @Inject constructor(
private val logDao: LogDao
): LoggerDataSource {
...
}
/**
* Scoping to the Activity container
*/
@ActivityScoped
class LoggerInMemoryDataSource @Inject constructor(
): LoggerDataSource {
...
}
At this moment, Hilt knows how to provide instances of LoggerInMemoryDataSource
and LoggerLocalDataSource
but doesn’t know which implementation to use when LoggerDataSource
is requested.
We know that we can use the @Binds
annotation to provide one LoggerDataSource
implementation, but if we need to provide both implementations in the same project?
Two implementations for the same interface
Since the different implementations of LoggerDataSource
are scoped to different containers, we cannot use the same module:
- LoggerInMemoryDataSource is scoped in Actiivty container
- LoggerLocalDataSource is scoped in Application container
We should define two different module:
@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemortModule {
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
@Binds method must have the scoping annotations if the type is scoped, so that’s why the functions above are annotated with @Singleton
and @ActivityScoped
. If @Binds
or @Provides
are used as a binding for a type, the scoping annotations in the implementation classes are not used anymore, so we can remove them.
If we buid the project right now, we’ll see DuplicateBindings
error:
[Dagger/DuplicateBindings] com.example.android.hilt.data.LoggerDataSource is bound multiple times
This is because the LoggerDataSource
type is being injected in Fragment
, but Hilt doesn’t know which implementation to use because there are two bindings of the same tyoe!
To tell Hilt how to provide different implementations (multiple bindings) of the same type, we can use qualifier annotation.
@Qualifier
annotation class InMemoryLogger
@Qualifier
annotation class DatabaseLogger
@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {
@DatabaseLogger
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@InMemoryLogger
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
Inject dependencies
We can make Hilt inject instances of different types with the @Inject
annotation on the fields we want to be injected:
@AndroidEntryPoint
class LoginFragment:Fragment() {
// note that fields injected by Hilt cannot be private.
@InmemoryLogger
@Inject lateinit var logger: LoggerDataSource
@Inject lateinit var formatter: Formatter
}