This is the fourth article in the MAD Skills series on Hilt! In this article, we’ll explore how to write a custom Hilt extension. For the first three articles in this series, see:
- Hilt is introduced
- Hilt testing best practices
- How Hilt works
If you prefer to see this in video, you can check it out here.
Example: WorkManager extension
A Hilt extension is a library that generates code, often through annotation handlers. The generated code acts as a module or entry point that forms the Hilt dependency injection diagram.
The WorkManager integration library in Jetpack is an example of an extension. The WorkManager extension helps us reduce the template code and configuration required to provide dependencies to workers. Hilt :hilt-work and AndroidX. hilt:hilt-compiler. The first part contains HiltWorker annotations and some run-time helper classes, and the second part is an annotation handler that generates modules based on the information provided by the annotations in the first part.
Using the extension is as simple as adding the @hiltworker annotation to your worker:
@HiltWorker
public class ExampleWorker extends Worker {
// ...
}
Copy the code
The extension compiler generates a class with the @Module annotation:
@Generated("androidx.hilt.AndroidXHiltProcessor")
@Module
@InstallIn(SingletonComponent.class)
@OriginatingElement(
topLevelClass = ExampleWorker.class
)
public interface ExampleWorker_HiltModule {
@Binds
@IntoMap
@StringKey("my.app.ExmapleWorker")
WorkerAssistedFactory<? extends ListenableWorker> bind(
ExampleWorker_AssistedFactory factory);
}
Copy the code
This module defines a binding for the worker that can access the HiltWorkerFactory. Then, configure the WorkerManager to use the Factory so that the worker’s dependency injection is available.
The Hilt polymerization
A key mechanism for enabling extensions is Hilt’s ability to discover modules and entry points from the classpath. This is called aggregation because modules and entry points are aggregated into an Application annotated with @HiltAndroidApp.
Because of Hilt’s aggregation capabilities, any tool that generates @Module and @entryPoint by adding @Installin annotations is discovered by Hilt and becomes part of the Hilt DI diagram at compile time. This allows the extension to be easily integrated into Hilt as a plug-in without any additional work by the developer.
Annotation processor
The normal way to generate code is to use an annotation handler. The annotation processor runs in the compiler before the source file is converted to a class file. When a resource has a supported annotation declared by the processor, the processor processes it. The processor can generate further methods that need to be processed, so the compiler loops through the annotation processor until nothing new is produced. Once all the work is done, the compiler converts the source file to a class file.
△ Annotation processing schematic diagram
Processors can interact with each other because of the loop mechanism. This is important because it allows Hilt’s annotation handler to handle @Module or @EntryPoint classes generated by other processors. This also means that your extensions can build on extensions written by others!
The WorkManager Extension Processor generates code from the class annotated with @HiltWorker, while validating annotation usage and using libraries such as JavaPoet to generate code.
Hilt extended annotations
There are two important annotations in the Hilt API: @Generatesrootinput and @originatingElement. Extensions should use these annotations to properly integrate with Hilt.
The extension should use @Generatesrootinput to enable code generated annotations. This lets the Hilt annotation handler know that it should finish extending the annotation handler before generating the component. For example, the @Hiltworker annotation itself is embellished by the @Generatesrootinput annotation:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@GeneratesRootInput
public @interface HiltWorker {
}
Copy the code
Generated classes with @module, @entrypoint, and @installin annotations need to add the @originatingelement annotation, whose input parameter is the top-level class that triggers Module or EntryPoint generation. This is how Hilt determines whether the generated modules and entry points are tested locally. For example, in a Hilt test, you define an inner class that adds the @HiltWorker annotation, and the initial element of the module is the test value.
Test cases are as follows:
@hiltAndroidTest class SampleTest {@hiltworker class TestWorker extends Worker {//... }}Copy the code
The generated module contains the @originatingElement annotation:
@Module @InstallIn(SingletonComponent.class) @OriginatingElement( topLevelClass = SampleTest.class ) public interface SampleTest_TestWorker__HiltModule {/ /... }Copy the code
tips
Hilt extensions support many possibilities. Here are some tips for creating extensions:
Common patterns in projects
If there are general patterns for creating modules or entry points in your project, they can most likely be automated using Hilt extensions. For example, if every class that implements a particular interface must create a module with multiple bindings, you can create an extension that generates multiple bindings simply by adding annotations to the implementation class.
Support for non-standard member injection
For those member injection types in the Framework that already support the ability to instantiate, we need to create @EntryPoint. Extensions that automatically create entry points can be useful if there are multiple types that need to be injected by members. For example, the library that needs to discover service implementations through ServiceLoader is responsible for instantiating the discovered service. To inject the dependency into the service implementation, you must create an @EntryPoint. With the Hilt extension, the automatic generation of entry points can be accomplished by adding annotations on the implementation class. Extensions can further generate code to use entry points, such as base classes that are implemented by services. This is similar to @androidentrypoint creating @entrypoint for an Activity and creating a base class that uses the generated EntryPoint to perform member injection in the Activity.
Mirror binding
Sometimes you need to use different qualifiers to mirror or redeclare the binding. This may be more common when there are custom components. To avoid losing redeclared bindings, you can create a Hilt extension to automatically generate modules for other mirrored bindings. For example, consider the case of “paid” and “free” subscriptions in an application with different dependency implementations. Then, each layer has two different custom components so that you can determine the scope of the dependencies. When adding a generic, unscoped binding, the module that defines the binding can include both components in its @installin, or it can be loaded in a parent component, usually a singleton. But when the binding is scoped, the module must be copied because different qualifiers are required. Implementing one extension can generate two modules, avoiding boilerplate code and ensuring that common bindings are not omitted.
conclusion
Extensions to Hilt can further enhance dependency injection capabilities in the code base because they can be integrated with other libraries that Hilt does not yet support. In summary, an extension usually consists of two parts, a run-time part that contains extension annotations, and a code generator (usually an annotation processor) that generates @Module or @EntryPoint. The runtime portion of the extension may have additional helper classes that are bound in the generated module or entry point using declarations. Code generators may also generate additional code related to extensions without having to specifically generate modules and entry points.
The extension must use two annotations to properly interact with Hilt:
- @generatesrootinput added to the extension annotations.
- OriginatingElement is added by an extension to a generated module or entry point.
Finally, you can take a look at the Hilt-Install-Binding project, an example of a simple extension that demonstrates the concepts mentioned in this article.
That’s the MAD Skills series on Hilt. To watch the full video, go to the Hilt-Mad Skills playlist. Thanks for reading this article!
Please click here to submit your feedback to us, or share your favorite content or questions. Your feedback is very important to us, thank you for your support!