A pragmatic guide to Hilt with Kotlin
An easy way to use dependency injection in your Android app
Hilt is a new dependency injection library built on top of Dagger that simplifies its use in Android apps. This guide showcases the core functionality with a few code snippets to help you get started with using Hilt in your project.
Setting up Hilt
To set up Hilt in your app, follow the Gradle Build Setup guide first.
After installing all the dependencies and plugins, annotate your Application
class with @HiltAndroidApp
to use Hilt. You don’t need to do anything else or otherwise directly invoke it.
Defining and injecting dependencies
When you write code that uses dependency injection, there are two major components to think about:
- Classes that have dependencies that you want to inject.
- Classes that can be injected as dependencies.
These are not mutually exclusive and, in many cases, your class is both injectable and has dependencies.
Make a dependency injectable
To make something injectable in Hilt, you must tell Hilt how to create an instance of that thing. These instructions are called bindings.
There are three ways to define a binding in Hilt.
- Annotate the constructor with
@Inject
- Use
@Binds
in a module - Use
@Provides
in a module
⮕ Annotate the constructor with @Inject
Any class can have a constructor annotated with @Inject
which makes it available as a dependency anywhere in your project.
⮕ Using a module
The other two ways of making something injectable in Hilt involve using modules.
A Hilt module can be thought of as a collection of “recipes” that tell Hilt how to create an instance of something that doesn’t have a constructor — such as an interface or a system service.
In addition, any module can be replaced in your tests with a different module. This makes it easy to replace interface implementations with mocks, for example.
Modules are installed in a Hilt component specified using the @InstallIn
annotation. I’ll explain this in more detail later.
Option 1: use @Binds
to create binding for an interface
If you want to use OatMilk
in your code when Milk
is requested, create an abstract method inside a module and annotate it with @Binds
. Note that OatMilk
must itself be injectable for this to work, which you can achieve by annotating its constructor with @Inject
.
Option 2: use @Provides
to create a factory function
When an instance can’t be constructed directly, you can create a provider. A provider is a factory function that returns an instance of an object.
An example of this is a system service such as ConnectivityManager
which needs to be obtained from a context.
The Context
object is injectable by default, as long as you annotate it with either @ApplicationContext
or @ActivityContext
.
Inject a dependency
Once your dependencies are injectable, you can inject them using Hilt in two ways.
- As constructor parameters
- As fields
⮕ As constructor parameters
If the constructor is marked with @Inject
, Hilt injects all of the parameters according to the bindings you defined for those types.
⮕ As fields
If the class is an entry point, here specified using the @AndroidEntryPoint
annotation (more about that in the next section), all fields annotated with @Inject
are injected.
Fields annotated with @Inject
must be public. It’s also convenient to make them lateinit
to avoid making them nullable, as their initial value prior to injection is null
.
Note that injecting dependencies as fields is only useful when your class must have a constructor without parameters, such as Activity
. In most cases, you’ll want to inject via constructor parameters instead.
Other important concepts
Entry point
Remember when I said that in many cases, your class is created by being injected and has dependencies injected into it? In some cases you’ll have a class that’s not created via dependency injection, but still has dependencies injected into it. A good example of this is activities, which are normally created by the Android framework rather than Hilt.
These classes are entry points into Hilt’s dependency graph and Hilt needs to know they have dependencies that need injecting.
⮕ Android Entry Point
Most of your entry points will be one of these so-called Android Entry Points:
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
If that’s the case, annotate it with @AndroidEntryPoint
.
⮕ Other entry points
Most apps only ever need Android Entry Points, but if you’re interfacing with non-Dagger libraries or Android components that are not yet supported in Hilt, you may need to create your own entry point to access the Hilt graph manually. You can read more about turning arbitrary classes into entry points.
ViewModel
A ViewModel
is a special case: it’s not instantiated directly, as the framework needs to create them, but is also not an Android Entry Point. Instead, ViewModel
s use the special @ViewModelInject
annotation which allows Hilt to inject dependencies into them when they’re created using by viewModels()
, similar to how @Inject
works for other classes.
If you need access to saved state in your ViewModel
, inject a SavedStateHandle
as a constructor parameter by adding the @Assisted
annotation.
To use @ViewModelInject
, you’ll need to add a few more dependencies. See Hilt and Jetpack integrations for more information.
Components
Each module is installed inside a Hilt component, specified by using @InstallIn(<component>)
. The module’s component is primarily used to prevent accidentally injecting a dependency in the wrong place. For example, @InstallIn(ServiceComponent.class)
would prevent bindings and providers in the annotated module from being used in an activity.
In addition, a binding can be scoped to the component the module is in. Which brings me to…
Scopes
By default, bindings are unscoped. In the example above, this means every time you inject Milk
, you get a new instance of OatMilk
. If you add the @ActivityScoped
annotation, you’ll scope the binding to ActivityComponent
.
Now that your module is scoped, Hilt will only create one OatMilk
per activity instance. In addition, that OatMilk
instance will be tied to that activity’s lifecycle — it will be created when the activity’s onCreate()
is called and destroyed when the activity’s onDestroy()
is called.
In this case, both milk
and moreMilk
will point to the same OatMilk
instance. However, if you have multiple instances of LatteActivity
, they will each have their own instance of OatMilk
.
Correspondingly, other dependencies injected into this activity have the same scope, and thus they too will use the same instance of OatMilk
:
The scope depends on the component your module is installed in, e.g. @ActivityScoped
can only be applied to bindings inside a module that is installed inside ActivityComponent
.
The scope also determines the lifecycle of injected instances: in this case, the single instance of Milk
used by Fridge
and LatteActivity
is created when onCreate()
is called for LatteActivity
— and destroyed in its onDestroy()
. This also means our Milk
wouldn’t “survive” a configuration change, as that would involve a call to onDestroy()
on the activity. You can overcome this by using a scope with a longer lifecycle, such as @ActivityRetainedScope
.
For a list of available scopes, components they correspond to, and the corresponding lifecycles they follow, see Hilt Components.
Provider injection
Sometimes you want more direct control over the creation of injected instances. For example, you may want to inject an instance — or several — of something only when it’s needed, depending on your business logic. In this case, you can use dagger.Provider
.
Provider injection can be used regardless of what the dependency is and how you inject it. Anything that can be injected can be wrapped inside Provider<…>
to make it use provider injection instead.
Dependency injection frameworks (like Dagger and Guice) are traditionally associated with large, complex projects. However, being both easy to get started with and simple to set up, Hilt brings all the power of Dagger in a package that can be readily consumed by any type of app, no matter how small or large a codebase it resides in.
If you’d like to learn more about Hilt, how it works, and other features you may find useful, head over to its official website where you can find a more detailed overview and reference documentation.