头图

This is the fourth article about Hilt MAD Skills series In this article, we will explore how to write a custom Hilt extension. If you need to know the first three articles in this series, please refer to:

If you prefer to learn about this content through video, you can click here view.

Case: WorkManager extension

Hilt extension is a library for generating code, often implemented by annotation processors. The generated code serves as the module or entry point that constitutes the Hilt dependency injection diagram.

The integrated library of WorkManager Jetpack is an example of an extension. The WorkManager extension helps us reduce the template code and configuration required to provide dependencies to workers. The library consists of two parts, androidx.hilt:hilt-work and androidx.hilt:hilt-compiler . The first part contains HiltWorker annotations and some runtime auxiliary classes. The second part is an annotation processor that generates modules based on the information provided by the annotations in the first part.

The extension is very simple to use, just add @HiltWorker annotation on your worker:

@HiltWorker
public class ExampleWorker extends Worker {
   // ...
}

The extension compiler will generate a class annotated with @Module:

@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);
}

This module defines a binding that can access HiltWorkerFactory for workers. Then, configure the WorkerManager to use the factory so that the dependency injection of the worker is available.

Hilt aggregation

A key mechanism for enabling extensions is Hilt's ability to discover modules and entry points from the classpath. This is called aggregation because the modules and entry points are aggregated into the Application annotated with @HiltAndroidApp.

Because Hilt has the ability to aggregate, any tool that generates @Module and @EntryPoint by adding @InstallIn annotations can be discovered by Hilt and become part of the Hilt DI diagram at compile time. This allows the extension to be easily integrated into Hilt in the form of a plug-in, without the developer having to deal with any additional work.

annotation processor

The conventional way to generate code is to use an annotation processor. Before the source file is converted into a class file, the annotation processor will run in the compiler. When the resource has a supported annotation declared by the processor, the processor will process it. The processor can generate further methods that need to be processed, so the compiler will continue to run the annotation processor in a loop until no new content is generated. Once all the steps are completed, the compiler will convert the source file into a class file.

△ 注解处理示意图

△ Annotation processing diagram

Due to the loop mechanism, the processors can interact. This is very important because it allows Hilt's annotation processor to process @Module or @EntryPoint classes generated by other processors. This also means that your extension can also be built on top of extensions written by others!

WorkManager extension processor generates code based on classes annotated with @HiltWorker, verifies the usage of the annotations and uses libraries such as JavaPoet

Hilt extended annotation

There are two important annotations in the Hilt API: @GeneratesRootInput and @OriginatingElement. Extensions should use these annotations to properly integrate with Hilt.

Extensions should use @GeneratesRootInput to enable annotations for code generation. This lets the Hilt annotation processor know that it should complete the work of extending the annotation processor before generating the component. For example, the @HiltWorker annotation itself is decorated by the @GeneratesRootInput annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@GeneratesRootInput
public @interface HiltWorker {
}

The generated classes with @Module, @EntryPoint, and @InstallIn annotations need to be added with @OriginatingElement annotations. The input parameters of this annotation are the top-level classes that trigger the module or entry point generation. This is the basis for Hilt to judge whether the generated modules and entry points are tested locally. For example, an internal class annotated with @HiltWorker is defined in the Hilt test, and the initial element of the module is the test value.

The test case is as follows:

@HiltAndroidTest
class SampleTest {
    @HiltWorker
    class TestWorker extends Worker {
        // …
    }
}

The generated module contains @OriginatingElement annotations:

@Module
@InstallIn(SingletonComponent.class)
@OriginatingElement(
    topLevelClass = SampleTest.class
)
public interface SampleTest_TestWorker__HiltModule {
    // …
}

experience

Hilt extension supports multiple possibilities. Here are some tips on creating extensions:

Common pattern in project

If you have common patterns for creating modules or entry points in your project, they can be automated by using Hilt extensions. For example, if every class that implements a specific interface must create a module with multiple bindings, then you can create an extension and simply add annotations to the implementation class to generate a multiple binding module.

supports non-standard member injection

For those member injection types that already support instantiation capabilities in the Framework, we need to create an @EntryPoint. If there are multiple types that need to be injected by members, it is useful to automatically create extensions for entry points. For example, the library realized by the service discovery service ServiceLoader In order to inject dependencies into the service implementation, an @EntryPoint must be created. By using the Hilt extension, you can add annotations to the implementation class to complete the automatic generation of entry points. Extensions can further generate code to use entry points, such as the base class that the service implements the extension. This is similar to @AndroidEntryPoint creating @EntryPoint for the Activity and creating a base class that uses the generated entry point to perform member injection in the Activity.

mirror binding

Sometimes it is necessary to use a different qualifier to mirror or redeclare the binding. This may be more common when there are custom components. In order to avoid losing the redeclared binding, you can create a Hilt extension to automatically generate other mirrored binding modules. For example, consider the case of "paid" and "free" subscriptions in an application that contains different implementations of dependencies. Then, each layer has two different custom components, so you can determine the scope of dependencies. When adding a universal binding with no scope, the module defining the binding can include two components in its @InstallIn, or it can be loaded in the parent component, usually a singleton component. But when the binding is scoped, the module must be copied because different qualifiers are required. Implementing one extension can generate two modules, which can avoid boilerplate code and ensure that common bindings are not missed.

summary

Hilt's extensions can further enhance the dependency injection capabilities in the code base, because they can be integrated with other libraries that Hilt does not yet support. All in all, an extension usually consists of two parts, a runtime part containing extension annotations, and a code generator (usually an annotation processor) that generates @Module or @EntryPoint. The runtime part of the extension may have additional auxiliary classes that are bound in the generated module or entry point using declarations. The code generator may also generate additional code related to the extension, and they do not need to specifically generate modules and entry points.

The extension must use two annotations to interact with Hilt correctly:

  • @GeneratesRootInput is added to the extension annotation.
  • @OriginatingElement is added by the extension to the generated module or entry point.

Finally, you can check out the hilt-install-binding project, which is a simple extended example that demonstrates the concepts mentioned in this article.

The above is MAD Skills series. If you want to watch the full set of videos, please move to Hilt-MAD Skills playlist . Thanks for reading this article!

Welcome to click here to submit feedback to us, or share your favorite content, problems found. Your feedback is very important to us, thank you for your support!


Android开发者
404 声望2k 粉丝

Android 最新开发技术更新,包括 Kotlin、Android Studio、Jetpack 和 Android 最新系统技术特性分享。更多内容,请关注 官方 Android 开发者文档。