[MUSIC PLAYING]
BRAD CORSO: Hi, everyone.
Welcome to the third episode of the Hilt MAD Skills series.
In this episode, we'll take a deep dive
into how Hilt works under the hood.
We'll first uncover how the various Hilt annotations
work together to generate code.
We'll then take a look at the Hilt Gradle plugin
and see how it works behind the scenes
to improve the overall experience when
using Hilt with Gradle.
Let's start by taking a look at how Hilt generates code.
As some of you probably already know,
Hilt use his annotation processors to generate code.
Annotation processing occurs within the compiler
when converting your source files into Java bytecode.
As the name suggests, annotation processors
are triggered on the annotations within your source files.
An annotation processor typically
inspects the annotation and types
to perform various tasks like validation or generating
new sources.
Let's take a look at some of the most important annotations that
can be used in Hilt.
Three of the most important annotations
are @AndroidEntryPoint, @InstallIn,
and @HiltAndroidApp.
Let's start with @AndroidEntryPoint.
As a brief review, @AndroidEntryPoint
enables field injection in your Android classes
such as activities, fragments, views, and services.
In this example, simply annotating
PlayActivity with @AndroidEntryPoint
allows us to inject the music player into our activity.
If you're using Gradle, you're probably
familiar with the simplified syntax shown in this example.
However, this syntax is actually just syntactic sugar
provided to you by the Hilt Gradle plugin.
We'll talk more about the Hilt Gradle plugin later.
But for now, let's look at how this example would look
without the syntactic sugar.
Now we see that the original base class, AppCompatActivity,
is really an input to the @AndroidEntryPoint annotation.
The play activity itself actually
extends a generated class, Hilt_PlayActivity.
This class is generated by Hilt's annotation processors
and contains all of the logic necessary to perform injection.
Let's take a deeper look.
In this example, the generated class
extends @AppCompatActivity.
However, in general, it will extend
whichever class was passed into the @AndroidEntryPoint
annotation.
This allows injection to work with any base class you want.
The main purpose of this generated class
is to handle injection.
It's important to perform injection as early as possible
to prevent accidental access to a field
before it's been injected.
Thus for activities, injection occurs during onCreate()
method.
Note that the code here has been simplified,
but it's close enough for the purposes of this discussion.
Now let's take a look at what the inject() method does.
To inject the activity, we first need
an instance of its injector.
In Hilt, the injector for an activity is an entry point.
And we can get an instance of the injector using
the entry points utility class.
In this example, the injector class
is called PlayActivity_Injector.
As you may have guessed, this class
is also generated by Hilt's annotation processors.
Let's take a closer look at the generated injector.
The generated injector is a Hilt entry point
that is installed into the activity component.
It contains a single method that allows
us to inject an instance of the play activity.
If you've ever used Dagger in an Android application
without Hilt, you're likely familiar with writing
these inject methods directly on the component.
Now that we've seen how @AndroidEntryPoint works,
let's look at the next annotation on the list,
@InstallIn.
As a quick review, @InstallIn is used
to indicate which component a module or entry point
should be installed into.
For example, here we've installed the music database
module into the singleton component.
With @InstallIn, a module or entry point
can be contributed from anywhere within
the transitive dependencies of your application.
However, at some point, we'll need
to collect all the @InstallIn contributions
to get the full set of modules and entry points
for each component.
Hilt generates metadata to make collecting these @InstallIn
contributions easier.
By putting the metadata into a fixed package,
Hilt's processors can easily find the metadata
generated in all transitive dependencies
of your application.
From there, we can use the information contained
in the metadata annotation to find
a reference to the @InstallIn contributions itself--
in this case, the music database module.
Now that we know about @AndroidEntryPoint
and @InstallIn, we're ready to take on the @HiltAndroidApp
annotation.
As a brief review, the @HiltAndroidApp annotation
enables injection in your Android application class.
In this regard, you can think of it
exactly like the @AndroidEntryPoint annotation.
However, @HiltAndroidApp has one other important function,
generating the components.
Let's take a look at how the components are generated.
To start, a user just needs to annotate their application
class with @HiltAndroidApp.
The Hilt annotation processor then generates a set
of components inside of a wrapper class that has the same
name as the application class prefixed with HiltComponents_.
If you've ever used Dagger before,
these components are the @Component and @Subcomponent
annotated classes that you normally would write by hand.
Hilt then looks in the metadata package
to find all of the @Install annotated classes.
The @InstallIn modules are placed
in the modules list of the corresponding component
declaration.
The @InstallIn entry points are placed
as supertypes of the corresponding component
declaration.
From here, the Dagger processor takes over
and generates the component implementation
from the @Component and @Subcomponent annotations.
If you've ever used Dagger without Hilt,
you've likely interacted with this class directly.
However, Hilt hides this complexity from the user.
As this talk is about Hilt, we won't
go into further details about the Dagger-generated code.
However, if you're interested, you
can check out this presentation by Ron Shapiro and David Baker,
and they'll walk you through the details.
In addition, you can check out the cheat sheet
for Dagger codegen 101.
Now that you've seen how code generation works in Hilt,
let's now take a look at the Hilt Gradle plugin.
The Hilt Gradle plugin performs a lot of useful tasks,
including bytecode rewriting and class path aggregation.
Let's first take a look at bytecode rewriting.
As the name suggests, bytecode rewriting
is the process of rewriting bytecode.
Unlike annotation processing, which can only
generate new code, bytecode rewriting
can rewrite existing code.
When used sparingly, this feature can be very powerful.
To motivate bytecode rewriting in Hilt,
let's look at a familiar example.
Previously, we looked at the @AndroidEntryPoint annotation
and how it's used to generate a base
class that performs injection.
While extending this base class works in practice,
it can cause quite a bit of issues with your IDE.
Since the generated class does not
exist until after you've successfully
compiled your code, you'll often see red squiggles in your IDE.
In addition, you won't have access
to autocomplete for things like overriding methods
and you won't be able to access methods from the base class.
Not only can this loss of features
slow down your coding velocity, but also
all these red squiggles can make it extremely difficult
to focus.
The Hilt Android plugin comes to the rescue
by enabling bytecode rewriting on your @AndroidEntryPoint
classes.
With the Hilt Android plugin enabled, all that's required
is to annotate your class with @AndroidEntryPoint,
and you can extend your normal base class.
During bytecode rewriting, the Hilt Gradle plugin
will swap your base class with the generated base class.
However, as this process happens directly in the bytecode,
it's invisible to the user.
I should also mention that there are some serious downsides
to bytecode rewriting.
First, changes have to be made using low-level bytecode rather
than source code.
In addition, because the bytecode has already
been compiled, any issues generally show up at runtime
rather than compile time.
Not only does this make rewriting bytecode risky,
but it also complicates debugging,
since when something does go wrong,
the source files may not represent the bytecode that's
being executed.
For these reasons, Hilt tries to rely on bytecode rewriting
as little as possible.
Finally, let's look at another useful feature
of the Hilt Gradle plugin called classpath aggregation.
To understand what classpath aggregation is
and why it's needed, let's look at another example.
In this example, app depends on a single Gradle module,
database, where both app and database contribute @InstallIn
modules.
As we've already seen, Hilt will generate metadata
into the fixed Hilt metadata package, which
will be used to find all @InstallIn annotated modules
when generating the component.
While this works fine for a single level of dependencies,
let's see what happens when we add another Gradle module
as a dependency of database.
In this case, we've added the Gradle module cache
as a dependency of database and a transitive dependency of app.
When cache is compiled, although it will generate metadata,
that metadata will not be available
when compiling app because it's a transitive dependency.
Thus, Hilt has no way of knowing about the cache module,
and it will be accidentally excluded
from the generated component.
While you could technically fix this issue by declaring cache
as an API dependency rather than implementation,
it's not recommended.
Not only is using API worse for incremental builds,
it's a nightmare to maintain.
This is where the Hilt Gradle plugin comes to the rescue.
The Hilt Gradle plugin automatically
aggregates all classes from the transitive dependencies of app
even if implementation is used.
In addition, the Hilt Gradle plugin
also has a number of benefits over using API directly.
First, classpath aggregation is less error-prone
and requires no maintenance compared
to manually using API for dependencies
throughout your app.
You can simply use implementation
as you normally would, and the Hilt Gradle plugin
will take care of the rest.
Second, the Hilt Gradle plugin only aggregates classes
at the application level.
So unlike when using API, compilation of your libraries
are not affected.
Finally, classpath aggregation provides better encapsulation
of your dependencies because it's
impossible to accidentally reference
these classes in your source files,
and they'll never show up as suggestions in code completion.
In this episode, we've uncovered how the various Hilt
annotations work together to generate code.
We also looked at the Hilt Gradle plugin
and saw how it works behind the scenes using bytecode rewriting
and classpath aggregation to make
using Hilt safer and easier.
If you want more information on Hilt,
visit these sites here to read up
on the latest information and guides.
Thanks for listening, and keep an eye out for more MAD skills
episodes to come.
Bye.
[MUSIC PLAYING]