Hilt is a new dependency injection library based on Dagger that simplifies the way Dagger is called in Android applications. This article provides a short code snippet to show you the core features of Hilt to help developers get started quickly.

Configuration Hilt

To configure Hilt in your application, see Gradle Build Setup.

After installing all the dependencies and plug-ins, simply add the @HiltAndroidApp annotation in front of your Application class to start using Hilt without doing anything else.

@HiltAndroidApp
class App : Application(a)Copy the code

Define and inject dependencies

When writing code that uses dependency injection, there are two important points to consider:

  1. Classes that you need to inject dependencies into;
  2. A class that can be injected as a dependency.

The two are not mutually exclusive, and in many cases, your class can inject dependencies as well as contain them.

Make dependencies injectable

If you need to make a class injectable in Hilt, you need to tell Hilt how to create instances of that class. This process is called bindings.

There are three ways to define bindings in Hilt:

  1. Add on the constructor@InjectAnnotations;
  2. Used on modules@BindsAnnotations;
  3. Used on modules@ProvidesAnnotation.

⮕ uses @inject annotations on constructors

The constructor of any class can annotate @Inject so that the class can be injected as a dependency throughout the project.

class OatMilk @Inject constructor() {
  ...
  }
Copy the code

⮕ User module

Two other ways to make a class injectable in Hilt are to use modules.

Hilt modules are “recipes” that tell Hilt how to create instances of classes that do not have constructors, such as interfaces or system services.

In addition, any module can be replaced by another module in your tests. This makes it easy to mock out interface implementations.

Modules are installed in specific Hilt components via the @installin annotation. I’ll talk more about this later.

Option 1: Create a binding for the interface using @Binds

If you want to use OatMilk instead in your code when Milk is needed, you can create an abstract method in the module and add the @Cursor-annotation to it. Note that OatMilk itself must be injects, just add @Inject annotation to OatMilk constructor.

interface Milk {... }class OatMilk @Inject constructor(): Milk {
  ...
}

@Module
@InstallIn(ActivityComponent::class)
abstract class MilkModule {
  @Binds
  abstract fun bindMilk(oatMilk: OatMilk): Milk
}
Copy the code

Option 2: Use @provides to create the factory function

When instances cannot be created directly, you can create a provider. A provider is a factory function that returns an instance of an object.

A typical example is system services, such as ConnectivityManager, whose instances need to be returned via a Context object.

@Module
@InstallIn(ApplicationComponent::class)
object ConnectivityManagerModule {
  @Provides
  fun provideConnectivityManager(
    @ApplicationContext context: Context
  ) = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
}
Copy the code

Context objects are injectable by default as long as you use the @ApplicationContext or @ActivityContext annotation.

dependent

When a dependency is injectable, you can use Hilt in two ways:

  1. Parameter injection as constructor;
  2. Injected as a field.

⮕ is injected as a constructor argument

interface Milk {... }interface Coffee {... }class Latte @Inject constructor(
  private val Milk milk,
  private val Coffee coffee
) {
  ...
}
Copy the code

If the constructor uses the annotation @inject, Hilt will Inject all parameters based on the binding you defined for the type.

⮕ is injected as a field

interface Milk {... }interface Coffee {... }@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
  @Inject lateinit var milk: Milk
  @Inject lateinit var coffee: Coffee

  ...
}
Copy the code

If the class is the entry point, specifically the class annotated with @AndroidEntryPoint (more on that in a later section), then all fields in the class that contain the @Inject annotation will be injected.

Fields annotated with @Inject must be of type public. You can also add LateInit to avoid null field values because they start with null before injection.

Note that the scenario as a field injection dependency is only suitable for cases where the class must contain a no-argument constructor, such as an Activity. In most scenarios, you would rather inject dependencies through constructor arguments.

Other important concepts

The entry point

Remember that I mentioned earlier that in many cases your classes will contain injected dependencies when they are created through dependency injection. In some cases, your classes may not have been created through dependency injection, but dependencies will still be injected. A typical example is an activity, which is created internally within the Android framework rather than by Hilt.

These classes are the entry points to the Hilt dependency graph, and Hilt needs to know that these classes contain dependencies to be injected.

⮕ Android entry point

Most of the entry points are so-called Android entry points:

  • Activity

  • Fragment

  • View

  • Service

  • BroadcastReceiver

If it is an Android entry point, please add the @AndroidEntryPoint annotation.

@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {... }Copy the code

⮕ Other entry points

Android entry points are sufficient for most applications, but if you use a library that does not contain a Dagger or an Android component that is not yet supported in Hilt, you may need to create your own entry points to manually access the Hilt dependency graph. See converting any class to an entry point for details.

ViewModel

The ViewModel is a special case: because the framework creates them, it is neither directly instantiated nor an Android entry point. The ViewModel needs to use the special @hiltViewModel annotation that enables Hilt to inject dependencies into the ViewModel when the ViewModel is created by byViewModels(). The principle is similar to @Inject annotation of other classes.

interface Milk {... }interface Coffee {... }@HiltViewModel
class LatteViewModel @Inject constructor(
  private val milk: Milk,
  private val coffee: Coffee
) : ViewModel() {
  ...
}

@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
  private val viewModel: LatteViewModel by viewModels()
  ...
}
Copy the code

If you need access to the cached state of the ViewModel, add the @assisted annotation, injecting SavedStateHandle as the constructor parameter.

@HiltViewModel
class LatteViewModel @Inject constructor(
  @Assisted private val savedState: SavedStateHandle,
  private val milk: Milk,
  private val coffee: Coffee
) : ViewModel() {
  ...
}
Copy the code

To use @ViewModelInject, you may need to add more dependencies. See the Hilt and Jetpack Integration Guide for more details.

component

Each module is installed in the Hilt component, specified by @installin (< component name >). The components of a module are mainly used to prevent accidental injection of dependencies into the wrong place. For example, @installin (Servicecomponent.class) prevents the binding and provider in the module that the annotation decorates from being called by the activity.

In addition, the scope of a binding is limited to the entire module to which the component belongs. Which is what we’re going to talk about…

scope

By default, bindings are not scoped. As in the above example, this means that each time Milk is injected, you get a new OatMilk instance. If you add the @activityScoped annotation, you limit the scope of the binding to the ActivityComponent.

@Module
@InstallIn(ActivityComponent::class)
abstract class MilkModule {
  @ActivityScoped
  @Binds
  abstract fun bindMilk(oatMilk: OatMilk): Milk
}
Copy the code

Now that your module is scoped, Hilt creates only one OatMilk instance per activity instance. In addition, OatMilk instances are bound to the activity lifecycle — it is created when the activity’s onCreate() is called and destroyed when the activity’s onDestroy() is called.

@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
  @Inject lateinit var milk: Milk
  @Inject lateinit var moreMilk: Milk // The instance here is the same as above. }Copy the code

In this case, milk and moreMilk point to the same OatMilk instance. However, if you have multiple LatteActivity instances, they will contain their own OatMilk instances.

Correspondingly, other dependencies injected into the activity have the same scope. So they’ll also cite the same OatMilk example:

// The Milk instance is created before the Fridge exists because it is tied to the activity lifecycle
class Fridge @Inject constructor(private val Milk milk) { ... }

@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
  // The following four entries share the same Milk instance
  @Inject lateinit var milk: Milk
  @Inject lateinit var moreMilk: Milk
  @Inject lateinit var fridge: Fridge
  @Inject lateinit var backupFridge: Fridge

  ...
}
Copy the code

Scope depends on the component your module installs, such as @ActivityScoped for binding only within the module installed in ActivityComponent.

The scope also determines the lifetime of the injected instance: In this case, a separate instance of Milk used by Fridge and LatteActivity is created when the LatteActivity onCreate() is called — and destroyed when onDestroy() is called. This also means that Milk is not “immune” to configuration changes, since onDestroy() is called when the configuration changes. You can avoid this problem by using scopes with longer lifetimes, such as @ActivityRetainedScope.

For a list of available scopes, related components, and the life cycle that follows, see Hilt Components.

The Provider injection

Sometimes you want to have more direct control over the creation of injected instances. For example, you might want to inject one or several instances of a type based on business logic. For such a scenario, you can use dagger.Provider:

class Spices @Inject constructor() {... }class Latte @Inject constructor(
  private val spiceProvider: Provider<Spices>
) {
  fun addSpices(a) {
    val spices = spiceProvider.get(a)// Create a new instance of Spices. }}Copy the code

Provider injection can ignore the specific dependency type and injection method. Anything that can be injected can be wrapped in Provider<… > to use provider injection.

Dependency injection frameworks (like Dagger and Guice) are often used for large and complex projects. Hilt is easy to use, very simple to configure, a self-contained code bundle, and a powerful feature of the Dagger that can be applied to all types regardless of code size.

If you want to learn more about Hilt, how it works, and other features that might be useful to you, please visit the official website for more detailed introductions and reference documentation.