Introduction to Hilt in the MAD Skills series

First episode of the Hilt MAD Skills series

Manuel Vivo
Android Developers

--

This is the MAD Skills article series on Hilt! In this article we’ll take a look at why dependency injection (DI) is important for your app and Hilt, Jetpack’s recommended solution for DI on Android.

If you prefer to consume this content in a video format, check it out here:

By following the principles of dependency injection in your Android app, you lay the groundwork for a good app architecture. It helps with reusability of code, ease of refactoring and ease of testing! Learn more about DI benefits here.

When creating instances of classes in your project, you can exercise the dependency graph manually by satisfying the dependencies and transitive dependencies that the class requires.

But doing this manually every time involves some boilerplate code and could be error-prone. See for example, one of the ViewModels that we have in iosched, the open source Google I/O app. Can you imagine the amount of code required to create a FeedViewModel with its dependencies and transitive dependencies?

It’s hard, repetitive, and we could easily get the dependencies wrong. By using a dependency injection library, we can get the benefits of DI without having to provide the dependencies manually, as the library generates all the necessary code for you. And here’s where Hilt comes into play.

Hilt

Hilt is a dependency injection library developed by Google that helps you get the most out of DI best practices in your app by doing the hard work and generating all the boilerplate you would’ve needed to write otherwise.

By using annotations, Hilt generates that code for you at compile time, making it really fast at runtime. This is done using the power of Dagger, the JVM DI library that Hilt is built on top of.

Hilt is Jetpack’s recommended DI solution for Android apps and it comes with tooling and other Jetpack libraries support.

Quick start

All apps that use Hilt must contain an Application class that is annotated with @HiltAndroidApp since it triggers Hilt’s code generation at compile time. And for Hilt to be able to inject dependencies into an activity, the activity needs to be annotated with @AndroidEntryPoint.

To inject a dependency, annotate the variables that you want Hilt to inject with @Inject. All Hilt-injected variables will be available when super.onCreate is called.

In this example, we’re injecting a MusicPlayer into PlayActivity. But how does Hilt know how to provide instances of type MusicPlayer? Well, it doesn’t at the moment! We need to let Hilt know how to do it… using annotations! of course.

Annotating the constructor of a class with @Inject tells Hilt how to create instances of that class.

This is all that’s needed to get a dependency injected into an Activity! That was quite easy! We started with a simple example as MusicPlayer doesn’t depend on any other type. But if we had other dependencies passed as a parameter, Hilt would take care of that and satisfy those dependencies when providing an instance of MusicPlayer.

This was in fact, a very simple and naive example. But if you had to do what we’ve done so far manually, how would you do that?

Doing it manually

When doing DI manually, you can have Dependency container classes that are responsible for providing types, and managing the lifecycle of the instances that it provides. Oversimplifying a bit here, that’s what Hilt does under the hood!

When you annotate the Activity with @AndroidEntryPoint, a dependency container is automatically created, managed, and associated to PlayActivity. Let’s call our manual implementation of it PlayActivityContainer. By annotating MusicPlayer with @Inject, we’re basically telling the container how to provide instances of type MusicPlayer.

And in the Activity, we’d need to create an instance of the container, and populate the dependencies of the activity using it. That’s also done by Hilt when annotating the activity with @AndroidEntryPoint.

Annotations recap

So far, we’ve seen that when @Inject is used to annotate the constructor of a class, it tells Hilt how to provide instances of that class. And when it annotates a variable in a @AndroidEntryPoint annotated class, Hilt injects an instance of that type into the class.

@AndroidEntryPoint, that can annotate most Android framework classes, not only activities, creates an instance of a dependency container for that class and populates all @Inject annotated variables.

@HiltAndroidApp annotates the Application class, and apart from triggering Hilt’s code generation, it also creates a dependency container associated with the Application class.

Hilt Modules

Now that we got the basics of Hilt covered, let’s complicate the example. Now, MusicPlayer takes a dependency in its constructor, MusicDatabase.

Therefore, we need to tell Hilt how to provide instances of MusicDatabase. When the type is an interface or you don’t own the class because it comes from a library, for example, you cannot annotate its constructor with @Inject!

Let’s imagine that we’re using Room as the persistence library in our app. Back to our manual implementation of PlayActivityContainer, when providing MusicDatabase, that with Room this would be an abstract class, we’d like to run some code when providing the dependency. Then, when providing an instance of MusicPlayer, we need to call the method that provides or satisfies the MusicDatabase dependency.

We don’t need to worry about transitive dependencies in Hilt, since it wires up all transitive dependencies automatically. However, we need to let it know how to provide instances of type MusicDatabase. For that, we use Hilt modules.

A Hilt module is a class annotated with @Module. And within the class, we can have functions that tell Hilt how to provide instances of certain types. This information known by Hilt are also called bindings in Hilt jargon.

The function that is annotated with @Provides tells Hilt how to provide instances of the MusicDatabase type. The body contains the block of code that Hilt needs to execute, and this is exactly the same as we had in our manual implementation of it.

The return type, MusicDatabase, informs Hilt about what type this function provides. And the function parameters tell Hilt the dependencies of the corresponding type, in this case, the ApplicationContext that is already available in Hilt. That code informs Hilt about how to provide instances of the MusicDatabase type, or in other words, we have a binding for MusicDatabase.

Hilt modules are also annotated with the @InstallIn annotation that indicates in which dependency containers or components this information is available. But what is a component? Let’s cover this in more detail.

Hilt Components

A Component is a class that Hilt generates that is responsible for providing instances of types, like the container we’ve been programming manually. At compile time, Hilt traverses the dependency graph of your application and generates code to provide all types with their transitive dependencies.

A Component is a class that Hilt generates that is responsible for providing instances of types

Hilt generates a Component, or dependency container, for most Android framework classes. The information, or bindings, of each component propagates through the components hierarchy.

Hilt’s components hierarchy

If the MusicDatabase binding is available in the SingletonComponent, that corresponds to the Application class, it’ll also be available in the rest of components.

These components are automatically generated by Hilt at compile time, and they’re created, managed, and associated with the corresponding Android framework class when you annotate those classes with @AndroidEntryPoint.

The @InstallIn annotation for modules is useful to control where those bindings are available and what other bindings they can use.

Scoping

Back to our manually created PlayActivityContainer code, I’m not sure if you realised but every time that the MusicDatabase dependency is required, we’re creating a different instance of it.

That’s not ideal since we might want to reuse the same instance of MusicDatabase throughout the whole app. Instead of a function, we could share the same instance by having all that in a variable.

Basically, we’re scoping the MusicDatabase type to this container as we’re always providing the same instance as a dependency. How to do this with Hilt? Well, no surprise here… With another annotation!

By using the @Singleton annotation in the @Provides method, we’re telling Hilt to always share the same instance of this type in that component.

@Singleton is a scope annotation. And each Hilt component has an associated scope annotation.

Scope annotations for each Hilt component

If you want to scope a type to the ActivityComponent, you’d use the ActivityScoped annotation. These annotations can be used in Modules but they can also annotate classes whose constructor is annotated with @Inject.

Bindings

There are two types of bindings:

  • Bindings that are NOT annotated with a scope annotation are called unscoped bindings, like MusicPlayer, and these bindings are available to all components if they’re not installed in a module.
  • Scoped bindings that are annotated with a scope annotation, like MusicDatabase, or unscoped bindings that are installed in a module are available in the corresponding component and the ones below it in the components hierarchy.

Jetpack Extensions

Hilt offers integrations with the most popular Jetpack libraries: ViewModel, Navigation, Compose and WorkManager.

Apart from ViewModel, each integration requires a different library to add to your project. Check the documentation for more information about it. Do you remember the FeedViewModel code from iosched that we saw at the beginning of the blog post? Do you want to see how it looks with Hilt support?

Apart from annotating the constructor with @Inject, to let Hilt know how to provide instances of this ViewModel, we need to annotate the class with @HiltViewModel.

That’s it! You don’t manually need to create a ViewModel provider for this, Hilt will take care of that.

Learn more!

Hilt is built on top of another popular dependency injection library: Dagger! Dagger will be often mentioned in the next episodes! If you’re using Dagger, Dagger and Hilt can work together. Read more about the migration APIs in the guide.

For more information about Hilt, we have a cheat sheet with the most popular annotations, what they do, and how to use them. Apart from our docs on Hilt, we also have codelabs to learn with a more hands-on experience.

And that’s it for this episode! But this doesn’t end here! We have more MAD skills episodes coming up, so please, follow the Android Developers Medium publication to see when they’re posted.

--

--