MAD Skills series: Hilt under the hood
By: Brad Corso
Tags: Dagger Hilt, Android, Android Development, Mad Skills, Dependency Injection

How the various Hilt annotations work together to generate code

Hilt uses annotation processors to generate code. Annotation processing occurs within the compiler when converting your source files into java bytecode. As the name suggests, annotation processors are triggered on the annotations within your source files. An annotation processor typically inspects the annotation and types to perform various tasks, like validation or generating new sources.

In Hilt, three of the most important annotations are @AndroidEntryPoint , @InstallIn , and @HiltAndroidApp .

@AndroidEntryPoint

AndroidEntryPoint enables field injection in your Android classes such as Activities, Fragments, Views, and Services.

In the following example, simply annotating PlayActivity with AndroidEntryPoint allows us to inject the MusicPlayer into our activity.

If you’re using Gradle, you’re probably familiar with the simplified syntax shown above. However, this syntax is actually just syntactic sugar provided to you by the Hilt Gradle plugin. More about the Hilt Gradle plugin below, but for now let’s look at how this example would look without the syntactic sugar.

Now we see that the original base class, AppCompatActivity , is really an input to the AndroidEntryPoint annotation. The PlayActivity , itself, actually extends the generated class Hilt_PlayActivity . This class is generated by Hilt’s annotation processors, and contains all of the logic necessary to perform injection. Here’s a simplified example of the code in that generated base class:

In this example, the generated class extends AppCompatActivity . However, in general it will extend whichever class was passed into the AndroidEntryPoint annotation. This allows injection to work with any base class you want.

The main purpose of this generated class is to handle injection. It’s important to perform injection as early as possible to prevent accidental access to a field before it’s been injected. Thus, for activities, injection occurs during the onCreate method.

In the inject method, we first need an instance of its injector, PlayActivity_Injector . In Hilt, the injector for an activity is an entry point, and we can get an instance of the injector using the EntryPoints utility class.

As you may have guessed, PlayActivity_Injector is also generated by Hilt’s annotation processors. It will have the following format:

The generated injector is a Hilt entry point that is installed into the ActivityComponent . It contains a single method that allows us to inject an instance of the PlayActivity . If you’ve ever used Dagger in an Android application without Hilt, you’re likely familiar with writing these inject methods directly on the component.

@InstallIn

InstallIn is used to indicate which component a module or entry point should be installed into. In the following example, we’ve installed the MusicDataBaseModule into the SingletonComponent :

With InstallIn , a module or entry point can be contributed from anywhere within the transitive dependencies of your application. However, at some point we’ll need to collect all of the InstallIn contributions to get the full set of modules and entry points for each component.

Hilt generates a metadata annotation in a fixed package to make collecting and discovering these InstallIn contributions easier. Generated annotations will have the following format:

By putting the metadata into a fixed package, Hilt’s processors can easily find the metadata generated in all transitive dependencies of your application. From there, we can use the information contained in the metadata annotation to find a reference to the InstallIn contribution itself — in this case, the MusicDatabaseModule .

HiltAndroidApp

Finally, the HiltAndroidApp annotation enables injection in your Android application class. In this regard, you can think of it exactly like the AndroidEntryPoint annotation. To start, a user just needs to annotate their application class with @HiltAndroidApp .

However, HiltAndroidApp has one other important function — generating Dagger components.

When the Hilt annotation processor encounters @HiltAndroidApp , it generates a set of components inside of a wrapper class that has the same name as the application class, prefixed with HiltComponents_ . If you’ve ever used Dagger before, these components are the @Component and @Subcomponent annotated classes that you normally would write by hand.

To generate these components, Hilt looks in the metadata package described above to find all of the @InstallIn -annotated classes. The @InstallIn modules are placed in the modules list of the corresponding component declaration. The @InstallIn entry points are placed as super types of the corresponding component declaration.

From here, the Dagger processor takes over and generates the component implementation from the @Component and @Subcomponent annotations. If you’ve ever used Dagger without Hilt, you’ve likely interacted with these classes directly. However, Hilt hides this complexity from the user.

As this blog post is about Hilt, we won’t go into further details about the Dagger generated code. However, if you’re interested, you can check out this presentation by Ron Shapiro and David Baker and they’ll walk you through the details. In addition, you can check out the cheat sheet for Dagger codegen 101.