A pragmatic guide to Hilt with Kotlin

An easy way to use dependency injection in your Android app

Filip Stanis
Android Developers

--

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:

  1. Classes that have dependencies that you want to inject.
  2. 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.

  1. Annotate the constructor with @Inject
  2. Use @Binds in a module
  3. 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.

  1. As constructor parameters
  2. 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, 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 @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.

--

--