Hilt Modules
Hilt modules are standard Dagger modules that have an additional
@InstallIn
annotation that determines which
Hilt component(s) to install the module into.
When the Hilt components are generated, the modules annotated with @InstallIn
will be installed into the corresponding component or subcomponent via
@Component#modules
or @Subcomponent#modules
respectively. Just
like in Dagger, installing a module into a component allows that binding to be
accessed as a dependency of other bindings in that component or any child
component(s) below it in the
component hierarchy. They can also be
accessed from the corresponding @AndroidEntryPoint
classes. Being installed in
a component also allows that binding to be scoped to that component.
Using @InstallIn
A module is installed in a Hilt Component by annotating the
module with the
@InstallIn
annotation. These annotations are required on all Dagger modules when using
Hilt, but this check may be optionally
disabled.
Note: If a module does not have an @InstallIn
annotation, the module will
not be part of the component and may result in compilation errors.
Specify which Hilt Component to install the module in by passing in the
appropriate Component type(s) to the @InstallIn
annotation.
For example, to install a module so that anything in the application can use it,
use SingletonComponent
:
@Module
@InstallIn(SingletonComponent.class) // Installs FooModule in the generate SingletonComponent.
final class FooModule {
@Provides
static Bar provideBar() {...}
}
@Module
@InstallIn(SingletonComponent::class) // Installs FooModule in the generate SingletonComponent.
internal object FooModule {
@Provides
fun provideBar(): Bar {...}
}
Each component comes with a scoping annotation that can be used to memoize a
binding to the lifetime of the component. For example, to scope a binding to the
SingletonComponent
component, use the @Singleton
annotation:
@Module
@InstallIn(SingletonComponent.class)
final class FooModule {
// @Singleton providers are only called once per SingletonComponent instance.
@Provides
@Singleton
static Bar provideBar() {...}
}
@Module
@InstallIn(SingletonComponent::class)
object FooModule {
// @Singleton providers are only called once per SingletonComponent instance.
@Provides
@Singleton
fun provideBar(): Bar {...}
}
In addition, each component has bindings that are available to it by default.
(See Hilt Components for a complete list.)
For example, the SingletonComponent
component provides the Application
binding:
@Module
@InstallIn(SingletonComponent.class)
final class FooModule {
// @InstallIn(SingletonComponent.class) module providers have access to
// the Application binding.
@Provides
static Bar provideBar(Application app) {...}
}
@Module
@InstallIn(SingletonComponent::class)
object FooModule {
// @InstallIn(SingletonComponent.class) module providers have access to
// the Application binding.
@Provides
fun provideBar(app: Application): Bar {...}
}
Installing a module in multiple components
A module can be installed in multiple components. For example, maybe you have a
binding in ViewComponent
and ViewWithFragmentComponent
and do not want to
duplicate modules. @InstallIn({ViewComponent.class,
ViewWithFragmentComponent.class})
will install a module in both components.
There are three rules to follow when installing a module in multiple components:
- Providers can only be scoped if all of the components support the same
scope annotation. For example, a binding provided in
ViewComponent
andViewWithFragmentComponent
can be@ViewScoped
because they both support that scope annotation. A binding provided inFragment
andService
can not be scoped with any of the standard scopes. - Providers can only inject bindings if all of the components have access to
those bindings. For example, a binding in
ViewComponent
andViewWithFragmentComponent
can inject aView
, whereas something bound inFragmentComponent
andServiceComponent
could not inject eitherFragment
orService
. - A child and ancestor component should not install the same module. (Just install the module in the ancestor, and the child will have access to those bindings).
App Build variants
Most Android apps will want to pull in different modules and bindings depending on the build variant of the app (e.g. production, debug, testing, etc.).
In Hilt, if your binary’s build target transitively depends on a module, then that module will be installed in the appropriate component for your app. This makes configuration as easy as defining a different build target and pulling different deps into your binary definition.
Bazel: Organizing your BUILD files
Because Bazel tends to enourage separation into finer-grained build targets, it is often better for tests to just avoid depending on modules you intend to replace in tests instead of uninstalling them. This is because it reduces the build dependencies of your test which can lead to overall faster build times.
When organizing your BUILD target for a module, you should consider if this module should be replaceable in tests or other configurations of your app. If it should never be replaced, then feel free to include the module with your other code sources.
If it should be replaceable though, you should create a separate target for your module. This target can then be pulled in at the root of your app so that each test root (or other configuration root) can decide whether to use your module or not.
There are two ways to organize your BUILD targets with regards to modules depending on the situation:
- Simply include modules with your normal build target. This will mean that users depending on your library will always get your definition.
- For those bindings that you want to be replaceable in tests, split your
modules out into a target called “module” that is meant to be depended on at
the
android_binary
level.
It is recommended to choose the first method by default and use the second method only for bindings that need to be replaceable in tests. It is expected, though, that many libraries will use both methods.
Hilt module visibility best practice
In Dagger, modules are usually public visibility because they are referenced by other components or other modules installing them. However, in Hilt, because modules are installed just by being in the transitive dependencies, modules don’t really need to be public for the same reason (technical aside: Hilt will actually generate public wrappers to get around visibility requirements for compilation).
In fact, doing the opposite and restricting visibility of Hilt modules is a best
practice because it prevents non-Hilt Dagger components from installing the
modules. Installing a Hilt module in a non-Hilt Dagger component would be
confusing because it wouldn’t be a component in the @InstallIn
annotation. For
libraries where you want a module for Hilt and non-Hilt users, it is usually
best to have two separate modules for each case. If the code is going to be the
same for both, have the Hilt module just be an empty module that uses
@Module(includes = ...)
to include the non-Hilt module.