Hilt's Two Phase Compilation and Incremental Builds
Hilt's Two Phase Compilation and Incremental Builds
Hilt is a compile time dependency injection framework for Android built on top of Dagger. Its most consequential architectural decision is a two phase compilation model that splits annotation processing into an isolating dependency-aggregation step and an aggregating root-component-generation step. This separation is what allows large, multi module projects to enjoy fast incremental builds: changing a single Hilt module triggers reprocessing of only that module's metadata, not the entire dependency graph. By the end of this lesson, you will be able to:
- Explain how Hilt's annotation processors split work into an isolating phase and an aggregating phase.
- Describe what the
@AggregatedDepsmetadata classes contain and why they exist. - Distinguish between ISOLATING, AGGREGATING, and DYNAMIC annotation processor modes and their impact on incremental compilation.
- Trace the lifecycle of a
@InstallInmodule from source annotation to generated Dagger component. - Apply the Hilt Gradle plugin's
enableAggregatingTaskoption and explain how it further improves build performance.
Phase 1: Dependency Aggregation in ISOLATING Mode
The first phase of Hilt's compilation is handled by the AggregatedDepsProcessor. This processor is declared as ISOLATING, which is the most incremental-friendly mode available to Java and Kotlin annotation processors. In ISOLATING mode, the processor is allowed to generate output files that depend only on the single annotated element being processed, not on any other source file in the compilation unit:
@IncrementalAnnotationProcessor(ISOLATING)
@AutoService(Processor.class)
public final class AggregatedDepsProcessor extends JavacBaseProcessingStepProcessor {
@Override
protected BaseProcessingStep processingStep() {
return new AggregatedDepsProcessingStep(getXProcessingEnv());
}
}
When you annotate a Dagger module with @Module and @InstallIn(SingletonComponent::class), this processor generates a small metadata class in a well-known package called hilt_aggregated_deps. The generated class is empty; its only purpose is to carry an @AggregatedDeps annotation that records which component the module belongs to:
package hilt_aggregated_deps
@AggregatedDeps(
components = "dagger.hilt.components.SingletonComponent",
modules = "com.example.NetworkModule"
)
public class _com_example_NetworkModule {}
This is a pure metadata artifact with no logic, fields, or methods. It exists solely so that a later compilation phase can discover it by scanning the hilt_aggregated_deps package and reading its annotation values. Because the processor runs in ISOLATING mode, Gradle knows that _com_example_NetworkModule depends only on NetworkModule.kt. If you edit NetworkModule, only that metadata class is regenerated.
The @AggregatedDeps annotation captures all the information Phase 2 needs without requiring access to the original source:
@Retention(CLASS)
public @interface AggregatedDeps {
String[] components(); // Target component(s)
String test() default ""; // Associated test class, if any
String[] replaces() default {}; // Modules this one replaces
String[] modules() default {}; // The module being installed
String[] entryPoints() default {};// The entry point being installed
}
The replaces field supports Hilt's testing infrastructure where test modules substitute production modules, and the entryPoints field handles @EntryPoint interfaces. All data is serialized into annotation values so Phase 2 can reconstruct the dependency graph from metadata alone.
Phase 2: Root Component Generation in AGGREGATING Mode
The second phase is handled by the RootProcessor, which is declared with DYNAMIC incremental mode. DYNAMIC means the processor decides at runtime whether to operate in ISOLATING or AGGREGATING mode, depending on configuration:
This interview continues for subscribers
Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.
Become a Sponsor