Component generation and module/entry point installation
Hilt generates components by finding all of the modules and entry points in the
transitive classpath. The @InstallIn
annotation on every module and entry
point generates a small metadata class in a defined package. The special package
is inspected when processing @HiltAndroidApp
to find all of the aggregated
items that need to be installed in the components. The same strategy is used for
other helper classes like @DefineComponent
and @AliasOf
.
Since the Android Application
is generated at the same time, the generated
Application
has a direct reference to the root generated component which is
the SingletonComponent
.
Since the HiltTestApplication
must support multiple tests, unlike in the
production application, reflection is used to find the generated components.
This is helpful because it allows the test application to be decoupled from
building with the tests which allows Hilt to provide a convenient default
instead of requiring each project to code generate a test application.
Reflection is not used in production because it provides less value and
reflection may have more costs.
Aggregating all of the modules in the classpath works well for tests because it
means tests can easily add bindings by just nesting classes in the test class
(or even better using @BindValue
which generates the module). Similarly, the
module detection also allows classes to embed @Module
classes as inner
classes. This can be used to ensure the class cannot be used without the
associated Dagger bindings and makes its usage less error prone (e.g. pairing a
class with an @BindsOptionalOf
it consumes or an @Binds
to an interface).
@AndroidEntryPoint
injection
@AndroidEntryPoint
works by generating a base class that the user code extends
either directly or indirectly via a transform in the
Gradle plugin. This base class is
responsible for retrieving the parent component (via Hilt interfaces on the
parent), creating the component, injecting the class, and exposing the component
to children via Hilt interfaces.
For example, to inject the activity the generated code essentially does the following (simplified for readability):
@Override public void onCreate(Bundled savedInstanceState) {
// This gets the parent component from the Application (in reality there is
// actually the activity retained component as the parent).
Object parentComponent =
((GeneratedComponentManager) getApplication()).generatedComponent();
// This creates the activity component. This involves an unsafe cast
// to know the parent component has the methods to build the activity component.
Object activityComponent = ((ActivityComponentBuilderEntryPoint) parentComponent)
.activityComponentBuilder()
.activity(this)
.build();
// This injects the activity. It also involves an unsafe cast to get access
// to the activity inject method. Like the other unsafe casts, these casts
// break build dependencies and are safe because they are code generated and
// guaranteed via the classpath discovery of modules/interfaces.
(MyActivity_GeneratedInjector) activityComponent).inject(this).
}
The generation of all of this glue code makes breaking dependencies with unsafe
casts safe and easy. Also, the automatic discovery combined with the fact that
the interfaces are generated with the activity that uses them makes it so that
including or removing an @AndroidEntryPoint
adds/takes all of its dependencies
with it. This allows apps built with Hilt to be modular.
Most of the time the parent component is easy to get, but in the case of views
and fragments it isn’t so easy because views get the activity context. To
support views with fragment bindings, the generated base class for fragments
override getLayoutInflater
to wrap the Context
in a ContextWrapper
that
holds the Dagger component for the view to get.
By standardizing all of these design decisions in Hilt, integrating libraries with activities/fragments/views should be much easier.