A practical guide to using Hilt with Kotlin

Prospective students on the course “Android Developer. Professional” we invite you to attend an open lesson on the topic “Writing the Gradle Plugin”


We also share the translation of a useful article.


An easy way to use dependency injection in Android apps

Hilt Is a new library for dependency injection built on top of Dagger… It allows you to use Dagger’s capabilities in Android apps in a simplified way. This tutorial describes the basic functionality of the library and provides some code snippets to help you get started using Hilt in your projects.

Setting up Hilt

To set up Hilt in your application, first follow the directions from the guide: Installing Gradle Build

After installing all the required elements and plugins to use Hilt, annotate your Application class @HiltAndroidApp. You don’t need to do anything else, and you don’t need to call Hilt directly.

Dependency Definition and Injection

When writing code that uses Dependency Injection, there are two main components to consider:

  1. Classes that have dependencies that you intend to inject.

  2. Classes that can be injected as dependencies.

They are not mutually exclusive: in many cases, a class is both injectable and has dependencies.

How to make a dependency injectable

To make an object embeddable in Hilt, you need to tell Hilt how to instantiate that object. Such instructions are called bindings

There are three ways to define anchor in Hilt.

  1. Add annotation to constructor @Inject

  2. Use @Binds in module

  3. Use @Provides in module

Adding Annotation to the Designer @Inject

Any class can have an annotated constructor @Inject, allowing it to be used as a dependency anywhere in the project.

Using the module

Two other ways to convert objects to embedded in Hilt are through the use of modules.

Hilt module Think of it as a set of “recipes” that tell Hilt how to instantiate something that doesn’t have a constructor, such as an interface or a system service.

In addition, in tests, any module can be replaced by another module. For example, this makes it easy to replace interface implementations with mock objects.

The modules are installed in Hilt componentwhich is indicated by annotation @InstallIn… I’ll give a more detailed explanation below.

Option 1: use @Binds to create a binding for the interface

If you want to use OatMilk in your code when you need Milk, create an abstract method inside the module and annotate it @Binds… Please note that for this option to work, OatMilk itself must be implemented. To do this, its constructor must be annotated @Inject

Option 2: create a factory function using @Provides

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

An example would be a system service, say the ConnectivityManager, that needs to be retrieved from the context.

The Context object is injectable by default when annotated @ApplicationContext or @ActivityContext

Dependency injection

Once you’ve made the dependencies you want to inject, there are two ways to inject them with Hilt.

  1. As constructor parameters

  2. Like fields

⮕ As constructor parameters

If you mark the constructor with annotation @Inject, Hilt will implement all the parameters according to the bindings you define for these types.

⮕ Like fields

If class is entry point specified using annotation @AndroidEntryPoint (more on this in the next section), all fields marked with the annotation will be embedded @Inject

Fields marked with annotation @Injectshould be publicly available. It is also convenient to mark them with the lateinit modifier so that they do not have to support empty values, since they have an initial value before implementation. null

Note that dependencies should be injected as fields only when the class must have a parameterless constructor, for example Activity… In most cases, we recommend that you inject dependencies through constructor parameters.

Other important concepts

Point of entry

Remember I said that in many cases the class is created by injecting and has dependencies embedded in it? In some cases, you will have a class that is not is created by dependency injection, but has dependencies injected into it. A good example of this is activities that are normally generated by the Android platform, not by the Hilt library.

These classes are entry points into the Hilt dependency graph, and Hilt needs to know that they have dependencies to inject.

⮕ Android entry point

Most of your entry points will be so called Android entry points:

  • Activity

  • Fragment

  • View

  • Service

  • BroadcastReceiver

If so, this entry point should be annotated @AndroidEntryPoint

⮕ Other entry points

Most applications usually only need Android entry points, but if you interact with libraries that don’t work with Dagger or Android components that are not yet supported by Hilt, then you may need to create your own entry point to manually access the Hilt graphics. You can read the instructions for converting arbitrary classes to entry points

ViewModel

The ViewModel is a special case: this class is not directly instantiated as it must be instantiated by the framework, nor is it an Android entry point. Instead, with classes ViewModel use special annotation @ViewModelInjectwhich allows Hilt to inject dependencies into them when they are created with the expression by viewModels()… This is similar to how @Inject works for other classes.

If you need access to the state stored in the class ViewModel, implement SavedStateHandle as a constructor parameter by adding annotation @Assisted

To use @ViewModelInject, you will need to add some more dependencies. For more information, see the article: Hilt and Jetpack integrations

Components

Each module is installed inside Hilt componentwhich is indicated by annotation @InstallIn(). The module component is mainly used to prevent accidental dependency injection in the wrong place. For example, the annotation @InstallIn(ServiceComponent.class) will not allow the use of bindings and providers available in the corresponding module inside the activity.

In addition, the use of a binding can be limited to the limits of the component in which the module resides. Which brings me to …

Areas

By default, anchors have no regions. In the example above, this means that every time you deploy Milk, you get a new instance of OatMilk. If you add annotation @ActivityScoped, the scope of the binding will be limited to ActivityComponent

Now that the module has a scope, Hilt will only create one OatMilk per activity instance. In addition, this OatMilk instance will be tied to the lifecycle of this activity – it will be created when you call onCreate() activity and destroyed when called onDestroy() activity.

In this case, both milk and moreMilk will point to the same OatMilk instance. However, if you have multiple LatteActivity instances, each will have its own OatMilk instance.

Accordingly, other dependencies injected into this activity will have the same scope, so they will also use the same OatMilk instance:

The scope depends on the component where your module is installed. For instance, @ActivityScoped can only be applied to bindings that are inside a module that is set inside ActivityComponent

The scope also defines the lifecycle of the embedded instances: in this case, the single Milk instance used by Fridge and LatteActivity is created when onCreate() called for LatteActivity, – and destroyed in it onDestroy()… This also means that our Milk will not “survive” the configuration change, since it calls onDestroy() for activity. You can overcome this by using an area with a longer life cycle, for example @ActivityRetainedScope

For a list of the areas available, the components they correspond to, and the life cycles they follow, see the article: Hilt Components

Supplier implementation

Sometimes you need more direct control over the creation of embedded instances. For example, you want one or more instances of an object to be injected only when needed, in accordance with business logic. In this case, you can use dagger.Provider

Provider injection can be used regardless of what the dependency is and how it is injected. Any object that can be embedded can be wrapped in Provider<…>to use vendor injection for it.

Dependency injection frameworks (such as Dagger and Guice) are commonly used in large-scale, complex projects. At the same time, the Hilt library, while being easy to learn and customize, provides all the features of Dagger in a package that can be used in any type of application, regardless of the size of the codebase.

For a closer look at the Hilt library, how it works, and other features you might find useful, head to its official websitefor a detailed overview and reference documentation.


Learn more about the course “Android Developer. Professional”

Sign up for an open lesson “Writing the Gradle Plugin”


Right now in OTUS started Christmas sale… The discount applies to absolutely all courses. Make a gift for yourself or loved ones – go to the site and pick up a discounted course. And as a bonus, we suggest registering for absolutely free demo lessons :

GET A DISCOUNT

Similar Posts

Leave a Reply