This article is the third article about Hilt MAD Skills series We will delve into how Hilt works. If you need to know the first two articles in this series, please refer to:
If you prefer to know the contents of this video, please click here view.
covered topics
- A variety of Hilt annotations work together and generate code.
- When Hilt is used with Gradle, how does the Hilt Gradle plugin work behind the scenes to improve the overall experience.
variety of Hilt annotations work together and generate code ways
Hilt uses annotation processors to generate code. The processing of annotations occurs during the conversion of the source file into Java bytecode by the compiler. As the name implies, the annotation processor acts on the annotations in the source file. The annotation processor usually checks the annotations and performs different tasks based on the annotation type, such as code inspection or generating new files.
In Hilt, the three most important annotations are: @AndroidEntryPoint , @InstallIn and @HiltAndroidApp .
@AndroidEntryPoint
AndroidEntryPoint Enable field injection in your Android classes, such as Activity, Fragment, View, and Service.
As shown in the following example, by adding AndroidEntryPoint
PlayActivity
, we can easily inject MusicPlayer into our Activity.
@AndroidEntryPoint
class PlayActivity : AppCompatActivity() {
@Inject lateinit var player: MusicPlayer
// ...
}
If you use Gradle, you may be familiar with the simplified syntax described above. But this is not the real syntax, but the syntactic sugar provided by the Hilt Gradle plugin. Next, we will explore more about the Gradle plugin. Before that, let's take a look at what this example should look like without syntactic sugar.
@AndroidEntryPoint(AppCompatActivity::class)
class PlayActivity : Hilt_PlayActivity() {
@Inject lateinit var player: MusicPlayer
// ...
}
Now, we see that the original base class AppCompatActivity 161723e56c364e is the real of the AndroidEntryPoint
PlayActivity
actually inherits the generated class Hilt_PlayActivity
, which is generated by the Hilt annotation processor and contains all the logic needed to perform the injection operation. For the base class generated above, the simplified code example is as follows:
@Generated("dagger.hilt.AndroidEntryPointProcessor")
class Hilt_PlayActivity : AppCompatActivity {
override fun onCreate() {
inject()
super.onCreate()
}
private fun inject() {
EntryPoints.get(this, PlayActivity_Injector::class).inject(this as PlayActivity);
}
}
In the example, the generated class inherits from AppCompatActivity
. However, under normal circumstances, the generated class will inherit the class annotated with AndroidEntryPoint
This allows injection operations to be performed in any base class you need.
The main purpose of generating classes is to handle injection operations. In order to avoid fields being accidentally accessed before injection, it is necessary to perform the injection operation as early as possible. Therefore, for Activity, the injection operation is performed in onCreate
.
In the inject method, we first need an instance of the injector- PlayActivity_Injector
. In Hilt, for Activity, the injector is an entry point. We can use the EntryPoints
obtain an instance of the injector.
As you might think, PlayActivity_Injector
is also generated by the Hilt annotation processor. The format is as follows:
@Generated("dagger.hilt.AndroidEntryPointProcessor")
@EntryPoint
@InstallIn(ActivityComponent::class)
interface PlayActivity_Injector {
fun inject(activity: PlayActivity)
}
The resulting injector is a Hilt entry point ActivityComponent
It only contains a method that allows us to inject PlayActivity
instance of 061723e56c370d. If you have used Dagger in an Android application (not through Hilt), you may be familiar with these injection methods written directly on the component.
@InstallIn
InstallIn used to indicate which component the module or entry point should be loaded into. In the following example, we load MusicDataBaseModule
into SingletonComponent :
@Module
@InstallIn(SingletonComponent::class)
object MusicDatabaseModule {
// ...
}
Through InstallIn
, modules and entry points can be provided in any transitive dependencies in the application. However, in some cases we need to collect all InstallIn
annotation to obtain the complete module and entry point of each component.
Hilt generates metadata annotations under specific packages to make it easier to collect and discover the content provided InstallIn
The generated annotation format is as follows:
package hilt_metadata
@Generated("dagger.hilt.InstallInProcessor")
@Metadata(my.database.MusicDatabaseModule::class)
class MusicDatabaseModule_Metadata {}
By putting the metadata into a specific package, the Hilt annotation processor can easily find the generated metadata among all transitive dependencies in your application. At this point, we can use the information contained in the metadata annotation to find the self-reference of the content provided InstallIn
In this example, it is MusicDatabaseModule
.
HiltAndroidApp
Finally, the HiltAndroidApp annotation allows your Android Application class to enable injection. Here, you can treat it as exactly the same AndroidEntryPoint
In the first step, developers only need to add @HiltAndroidApp
annotations to the Application class.
@HiltAndroidApp
class MusicApp : Application {
@Inject lateinit var store: MusicStore
}
However, HiltAndroidApp has another important role-to generate Dagger components.
When the Hilt annotation processor encounters the @HiltAndroidApp
annotation, it will generate a series of components in the packaging class, the packaging class has the same name as the Application class, and the prefix is HiltComponents_
. If you have used Dagger before, these components are @Component
and @Subcomponent
, and you usually need to write them manually in Dagger.
In order to generate these components, Hilt looks for all the classes annotated with @InstallIn
The module with the @InstallIn
is placed in the module list of the corresponding component declaration. The entry point with the @InstallIn
annotation is placed in the position where the parent type of the corresponding component is declared.
From here, the Dagger processor takes over and generates specific implementations of components @Component
and @Subcomponent
If you have used Dagger before (not through Hilt), then there is a high probability that you have directly dealt with these classes. However, Hilt hides this complicated operation from developers.
This is an article about Hilt, we won't introduce the code generated by Dagger in detail. If you are interested, please refer to:
- Ron Shapiro and David Baker's lecture .
- Dagger codegen 101's cheat sheet .
Hilt Gradle plugin
Now that you have understood the working principle of code generation in Hilt, let's take a look at the Hilt Gradle plug-in . The Hilt Gradle plugin performs many useful tasks, including bytecode rewriting and classpath aggregation.
bytecode rewriting
As the name implies, bytecode rewriting is the process of rewriting bytecode. Unlike annotation processing that can only generate new code, bytecode rewriting can modify existing code. If used with caution, this will be a very powerful feature.
To illustrate why we use bytecode rewriting in Hilt, let's go back to @AndroidEntryPoint
.
@AndroidEntryPoint(AppCompatActivity::class)
class PlayActivity : Hilt_PlayActivity {
override fun onCreate(…) {
val welcome = findViewById(R.id.welcome)
}
}
Although inheriting the Hilt_PlayActivity
base class is effective in practice, it may cause IDE errors. Since the generated class does not exist until you successfully compile the code, you will often see red wavy lines in the IDE. In addition, you will not be able to enjoy automatic completion capabilities such as method overloading, and you will not be able to access methods in the base class.
Losing these features will not only slow down your coding speed, but these red wavy lines will also greatly distract you.
The Hilt Android plugin initiates bytecode rewriting AndroidEntryPoint
After enabling the Hilt Android plug-in, you only need to add @AndroidEntryPoint
annotations to the class, and you can make it inherit the normal base class.
@AndroidEntryPoint
class PlayActivity : AppCompatActivity { // <-- 无需引用生成的基类
override fun onCreate(…) {
val welcome = findViewById(R.id.welcome)
}
}
Since this grammar does not need to reference the generated base class, it will not cause the IDE to report an error. During bytecode rewriting, the Hilt Gradle plugin will replace your base class with Hilt_PlayActivity
. Since this process directly manipulates the bytecode, it is invisible to developers.
However, bytecode rewriting still has some disadvantages:
- The plug-in must modify the underlying bytecode, not the source code, which is error-prone.
- Because the bytecode has been compiled during the rewrite operation, the problem usually occurs at runtime rather than at compile time.
- The rewrite operation complicates debugging, because when a problem occurs, the source file may not represent the bytecode currently being executed.
For these reasons, Hilt tries to reduce the reliance on bytecode rewriting as much as possible.
class path aggregation
Finally, let us look at another useful feature of the Hilt Gradle plugin: classpath aggregation. To understand what classpath aggregation is and why it is needed, let's look at another example.
In this example, :app
relies on an independent Gradle module :database
. :app
and :database
provide modules annotated InstallIn
As you can see, Hilt will generate metadata under a specific hilt_metadata package. When generating components, it will use them to find all modules annotated with @InstallIn.
Processing without classpath aggregation still works fine for single-layer dependencies, now let's see what happens when another Gradle module :cache
as a dependency of :database
When :cache
is compiled, although it generates metadata, the metadata cannot be used when :app
Therefore, Hilt has no way of knowing about CacheModule
and it will be accidentally excluded from the generated components.
Of course, you can use api instead of implementation
declare :cache
to solve this problem at the technical level, but this is not recommended. Using APIs not only makes incremental builds worse, but also makes maintenance a nightmare.
This is where the Hilt Gradle plugin comes into play.
Even with implementation
, the Hilt Gradle plugin can automatically aggregate all classes from the transitive dependencies of :app
In addition, Hilt Gradle plug-in has many advantages compared api
First, compared to manually using api
dependencies in the entire application, classpath aggregation is less error-prone and requires no maintenance. You can simply use implementation
as usual, and the rest will be handled by the Hilt Gradle plugin.
Secondly, the Hilt Gradle plugin only aggregates classes at the application level, so unlike using api, the compilation of libraries in the project is not affected.
Finally, classpath aggregation provides better packaging for your dependencies, because it is impossible to accidentally reference these classes in the source file, and they will not appear in the code completion prompt.
summary
In this article, we reveal how various Hilt annotations work together to generate code. We also paid attention to the Hilt Gradle plugin and learned how it uses bytecode rewriting and classpath aggregation behind the scenes to make Hilt use safer and easier.
The above is the entire content of this article. We will release more MAD Skills articles soon, so stay tuned for subsequent updates.
Welcome to click here to submit feedback to us, or share your favorite content or problems found. Your feedback is very important to us, thank you for your support!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。