5 minute guide on understanding Hilt annotations

Anitaa Murthy
5 min readJun 26, 2024

--

Hilt is a dependency injection library for Android that reduces the boilerplate of doing manual dependency injection in your project. It is built upon the well-established Dagger2 library.

Hilt annotations are special words or phrases that are used to help the Hilt library work properly in an Android project. This guide shares some of the basic annotations you will come across in any Hilt injected android projects along with sample code.

@HiltAndroidApp

The @HiltAndroidApp annotation is typically applied to the Application class of our app. This internally generates a base class that initialises Hilt and also sets up the dependency injection framework for the application.

Note that we would need to add the Application class to our AndroidManifest.xml file in order to initialise Hilt.

SampleApplication.kt
AndroidManifest.xml

@AndroidEntryPoint

In order to inject dependencies in any Android component classes, we need a Hilt component for that just like our Application class. By applying @AndroidEntryPoint to a component, Hilt generates the necessary code to handle dependency injection for that component. Similarly, to enable injection of a ViewModel by Hilt use the @HiltViewModel annotation.

Note: If we annotate an Android class with @AndrodEntryPoint, then we also must annotate Android classes that depend on it. For example, if we annotate a fragment, then we must also annotate any activities where we use that fragment.

MainActivity.kt
MainViewModel.kt

@Inject

The @Inject annotation is used to mark dependencies that need to be injected into a class. It is a general-purpose annotation that is used to indicate fields, methods or constructors that should be injected with their dependencies. There are two ways to inject any object into a class: Constructor Injection and Field Injection.

Constructor Injection
Field Injection

Here, the mainRepository field is annotated with @Inject. Hilt will automatically inject an instance of MainRepository into this field when creating an instance of MainActivity. Note: fields injected by Hilt cannot be private. Attempting to inject a private field with Hilt results in a compilation error.

Best practice tip: Constructor injection is the recommended approach for injecting dependencies in Hilt. It ensures that dependencies are provided during object creation and promotes better testability and maintainability of our code. Avoid using field injection or method injection unless necessary.

@Module

The @Module annotation is used to define a module class. A module class provides instructions to the Hilt dependency injection framework on how to create and provide instances of dependencies. This is particularly helpful because there are instances when a class cannot be instantiated using a constructor. For example, injecting interfaces, injecting third party classes or injecting classes created by the Builder pattern. In these cases, we can provide Hilt with binding information by using Hilt modules.

ApiModule.kt

@InstallIn — The @InstallIn annotation is used to specify the component in which the module should be installed. It is used in alongside the @Module annotation to define the scope and visibility of the module’s dependencies.

In the above example, we are telling Hilt that all the instances you will create in this module lives as long as the application lives. Similarly, when we need activity or fragment scoped instances, we can use ActivityComponent or FragmentComponent respectively in our module.

@Binds vs @Provides

  • @Binds annotation tells Hilt which implementation to use when it needs to provide an instance of an interface.
  • @Provides is responsible for creating and returning instances of the corresponding dependencies. Using @Provides in a module allows us to have more control over how dependencies are created and provided so that we can customise initialisation or configuration of dependencies.

Injecting Interface instances with @Binds

ApplicationModule.kt — Useful for injecting interface instances

Injecting instances with @Provides

ApiModule.kt

In the above example, we are injecting instances of classes which are part of the Retrofit library. This one will not be abstract as this time we have to add definitions to our functions which will tell Hilt how to instantiate the Retrofit Builder.

@Qualifier

There are use cases in Android when we would want to create multiple instances of a class with the same name but with different implementations. i.e. two functions have same return type but different body. For such scenarios Hilt provides the @Qualifier annotation. This is something that we can use to identify a specific binding for a type when that type has multiple bindings defined.

https://gist.github.com/anitaa1990/0b34e8e624f57da99601803844a3eba9

Scope Annotations

By default, all bindings in Hilt are unscoped .i.e. every time I inject a binding, a new instance of that type will be created. In order for us to define the same instance of a binding throughout our application or activity, we need to add scope to the bindings.

Different components have different scopes and different lifecycles. Components are what we define when we create a Hilt module using the @InstallIn annotation.

Best practice tip: Scoping a binding to a component can be costly because the provided object stays in memory until that component is destroyed. Minimize the use of scoped bindings in your application.

@EntryPoint

By default, Hilt does not provide annotations for all android classes (eg: Adapters, ContentProviders etc). In order to be able to inject dependencies to non-android classes, we need to define an interface annotated with @EntryPoint . This allows accessing dependencies without using the @AndroidEntryPoint annotation on an Android component.

To access the dependencies provided by the entry point, we need to inject EntryPointAccessors and use it to get an instance of the entry point.

NotificationBuilder.kt

Bonus: @AssistedInject

The @AssistedInject is used to construct an instance where some parameters can be provided at the runtime (or the time of creation).

For example, let’s say we have a MainViewModel.kt class that requires a userID String, whose value is decided at runtime.

Step 1: Create a ViewModel class using Assisted Injection

MainViewModel.kt

Compared to normal Injection, the differences here are:

  1. We use @AssistedInject instead of @Inject for constructor injection.
  2. Arguments which need to be provided at runtime are annotated with @Assisted.

Step 2: Create a factory for the MainViewModel class

In order for us to inject our ViewModel class to our Activity/Fragment, we need to create a factory for it. We will use that factory to create an instance of the ViewModel class.

MainViewModel.Factory.kt

We create a Factory for our class as an interface and annotate it with @AssistedFactory. This annotation tells Hilt that this interface is used to create an instance of a class/viewmodel that requires Assisted Injection. Inside this factory, we create a function named “create” that will be responsible for returning an instance of our ViewModelclass and accepts only those arguments which are to be provided by us at runtime (i.e. userId).

Step 3: Using our ViewModel class inside the Activity/Fragment

MainActivity.kt

And that’s it!

I hope this was useful! Happy coding!

--

--