Hilt Extensions in the MAD Skills series
By: Daniel Santiago
Tags: Extension, Annotation Processor, Dagger Hilt, Android, Dependency Injection

This is the fourth episode in the MAD Skills series on Hilt! Be sure to check out Episode 1 — Introduction to DI and Hilt, Episode 2 — Testing best practices, and Episode 3 — Hilt under the hood. In this episode, we’ll take a look at how to write your own Hilt extensions.

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

Example: WorkManager Extension

Hilt extensions are libraries that generate code, usually via an annotation processor. The code generated are modules or entry points that contribute to Hilt’s dependency injection graph.

One example of an extension is the integration library for Jetpack’s WorkManager. The WorkManager extension helps reduce the boilerplate and setup required to provide dependencies to workers. The library is composed of two artifacts, androidx.hilt:hilt-work and androidx.hilt:hilt-compiler. The first artifact contains the HiltWorker annotation along with some runtime helper classes, while the second artifact is an annotation processor that generates modules with the information provided by the annotations of the first artifact.

Using the extension is quite easy, simply annotate your worker with @HiltWorker:

Then the extension compiler will generate a @Module annotated class:

The module defines a binding for the worker that is accessible to the HiltWorkerFactory. WorkManager is then configured to use the factory, enabling dependency injection of workers.

Hilt Aggregation

A key mechanism that enables extensions is the ability for Hilt to discover modules and entry points in the classpath. This is referred to as aggregation since the modules and entry points are aggregated into the application annotated with @HiltAndroidApp.

Thanks to Hilt’s aggregation, any tool that generates @Module or @EntryPoint annotated classes with @InstallIn will be discovered by Hilt and become part of the Hilt DI graph during compilation. This makes it easy for extensions to plug into Hilt without any extra work from the user.

Annotation Processor

One common way to generate code is via an annotation processor. Annotation processors run within the compiler before converting the sources into class files. A processor will run when a source is found with one of the supported annotations declared by the processor. Processors can generate code that can be further processed, therefore the compiler keeps running annotation processors in rounds until no new generated source is produced. Once all rounds are over the compiler then converts the sources into class files.

Annotation Processing Diagram

Due to the multiple rounds mechanics, processors can interact with each other. This is important because this allows Hilt’s annotation processors to process @Module or an @EntryPoint generated classes by other processors. This also means your extension can build on top of extensions that others write as well!

The WorkManager extension processor generates code based on classes annotated with @HiltWorker, validating the annotation usage and using libraries such as JavaPoet to generate code.

Hilt Extension Annotations

There are two important annotations in Hilt’s API that extensions should use in order to properly integrate with Hilt. These are @GeneratesRootInput and @OriginatingElement.

The annotation that triggers code generation by the extension should be annotated with @GeneratesRootInput. This lets Hilt’s annotation processor know it should wait for the extension annotation processor to be done before generating the components. As an example, the @HiltWorker annotation is itself annotated with @GeneratesRootInput:

A generated class that is annotated with @Module or @EntryPoint along with @InstallIn should also be annotated with @OriginatingElement, whose value is the top level class that caused the module or entry point to be generated. This is so Hilt can identify whether the generated module or entry point is local to a test or not. For example, if an inner class that is @HiltWorker is defined within a Hilt test then the originating element for the module is the test.

For the following test:

The generated module will contain the following @OriginatingElement:

Ideas

There is a wide range of possibilities that Hilt extensions enable, here are some ideas for creating an extension:

Common patterns used in projects.

If you have a common pattern in your project that creates modules or entry points, then it is very likely they can be automated with a Hilt extension. For example, if for every class that implements a certain interface a module with a multibinding binding must be created, then it is possible to create an extension where by just annotating the implementation class the multibinding module is generated.

Supporting non-standard member injection.

To member-inject types that are part of some other framework that owns the instantiation, then one needs to create an @EntryPoint. If there are multiple types that need to be member injected, then an extension to automate creating the entry points can be useful. For example, a library that discovers implementations of a service via a ServiceLoader is responsible for instantiating the discovered services. In order to inject dependencies into the service implementation an @EntryPoint must be created. With a Hilt extension it is possible to instead annotate the implementation class such that an entry point is automatically generated. The extension can further generate code to use the entry point, such as a base class that is then extended by the service implementation. This is similar to how @AndroidEntryPoint creates an @EntryPoint for an Activity and also creates a base class that uses the generated entry point to perform member injection in the activity.

Mirroring bindings

Sometimes it is the case that bindings need to be mirrored or redeclared with different qualifiers. This might be more common when custom components are present. In order to avoid missing a redeclaration, a Hilt extension can be created to automatically generate modules that mirror other bindings. For example, consider the case of a ‘paid’ and ‘free’ subscription of an app containing different dependency implementations. You then have two different custom components for each tier so you can scope dependencies. When adding a common non-scoped binding the module that defines the binding can have both components in its `@InstallIn` or can be installed in the parent component, usually the Singleton component. But when the binding is scoped the module has to be duplicated since different qualifiers are needed. An extension could instead generate both modules, avoiding the boilerplate code and making sure common bindings are not missed.

Summary

Extensions for Hilt can further empower dependency injection in your codebase since they can be authored to integrate with other libraries not yet supported by Hilt. To summarize, an extension will usually be composed of two parts. A runtime artifact containing an annotation for the extension, and a code generator, usually an annotation processor, that generates @Module or @EntryPoint. The runtime aspect of the extension might have additional helper classes that use the bindings declared in the generated modules or the entry points. The code generator might also generate additional code relevant to the extension, i.e. they don’t need to exclusively generate modules and entry points.

Two annotations must be used for the extension to correctly interact with Hilt: