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
@Bindsin a module
@Providesin a module
⮕ Annotate the constructor with
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
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.
Context object is injectable by default, as long as you annotate it with either
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
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
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:
If that’s the case, annotate it with
⮕ 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 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,
ViewModels 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
@ViewModelInject, you’ll need to add a few more dependencies. See Hilt and Jetpack integrations for more information.
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…
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
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
moreMilk will point to the same
OatMilk instance. However, if you have multiple instances of
LatteActivity, they will each have their own instance of
Correspondingly, other dependencies injected into this activity have the same scope, and thus they too will use the same instance of
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
The scope also determines the lifecycle of injected instances: in this case, the single instance of
Milk used by
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
For a list of available scopes, components they correspond to, and the corresponding lifecycles they follow, see Hilt Components.
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
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.