[MUSIC PLAYING]
DANIEL SANTIAGO: Hi.
My name is Daniel Santiago, and on this Hilt MAD Skills Series
video, I'll be talking about extensions for Hilt. This will
be a short and sweet video.
We'll go over some real examples of an extension as a motivation
starter.
We'll show a simpler idea, because they can get complex.
But a simpler idea can help you get started.
We'll show you how to generate some code,
because an extension does generate some code.
And then at the end, we'll go over a small summary and more
practical ideas for extensions.
So you might be wondering, what exactly is a Hilt extension?
Well, in essence, it's some sort of code generating tool that
creates modules and entry points for Hilt. The modules add
new bindings and the entry points
enable some sort of injection to non-supportive frameworks
or to access those bindings directly.
Along with the code generator tool,
there's some helper classes and an additional annotation.
Extensions are really useful because they
can help you support patterns and libraries without having
to wait for Hilt to do so.
One real world example of an extension
is WorkManager extension.
This extension is not part of core Hilt
even though it was part of the initial release.
The extension is divided into two parts, into two artifacts.
HiltWork defines the HiltWorker that kicks off the annotation
processor.
It also has a helper class that you might be familiar with,
HiltWorkerFactory.
And then the HiltCompiler is the other side
of the extension that has an annotation processor that
generates code.
Let's see how it works.
It's really easy.
You have a Worker annotated with HiltWorker
and then a module gets generated for it.
The module basically binds the worker into a multi map.
And now in the runtime part in HiltWork,
there's a Hilt module that uses that multi map and creates
the helper class, the HiltWorkerFactory, which
is what you configure WorkManager with.
And that's it.
Your Workers are now dependency injected.
The real reason this works well, as in the reason
you're able to generate modules and entry point
and they get aggregated to Hilt, is
because Hilt has some mechanism for aggregating
these sort of classes.
And this is key to enabling extensions.
Hilt has this mechanism which basically
finds modules and entry points along your classpath to install
them on the right components.
So if you have an app, this app has modules,
and it depends on other libraries
and they all have modules that get installed,
Hilt will be able to go along your transitive dependencies
and aggregate those modules and entry points into the root
module, which is your app.
This is important because you'll be able to generate code,
and then Hilt will pick that up.
Now, let's go over a more simple example.
All the code that I'm about to show you,
it's on GitHub, which is you can look it up
and it'll be a good starting point
for creating an extension.
In this example, we basically want
to reduce some boilerplate code, especially
if we have a single interface implementation.
So assume we have an authenticator interface
and we bind it into an implementation,
you will usually write a module for it with a method,
with a Binds.
So, we want to reduce this module,
we basically want to automate it by generating it.
Be aware, this example is very simple.
The value proposal is very small,
but we'll talk about some more realistic ideas later on.
The way we would do this is we create a custom annotation,
let's call it InstallBinding.
And we just put it on our implementation.
And then what we would do is we would
generate that equivalent module that we would have written.
Now, you might be wondering, OK, so how
do we generate this code?
Well, we can use an annotation processor.
Annotation processor is a process
that occurs within the compiler when converting sources
into classes.
Basically, before the source is actually compiled,
the compiler will look for annotation processors
registered to process certain annotations,
and they'll run them.
And if these processors generate new sources,
then it'll go ahead and see if they
can be processed by another annotation processor and so on.
And as long as sources are generated,
more rounds will be run until eventually no new sources
are generated.
And then your sources get compiled, whether they
were generated or handwritten.
So if we go back to our example, if we
had an InstallBinding on our implementation, what we would
do is we generate a module that would be equivalent to what
we have handwritten.
And because of the aggregated mechanism,
Hilt will be able to pick this up,
and that binding will be available.
Now, there's two important parts to create an extension.
You do have to use these two extensions shown
here, the GeneratesRootInput and OriginatingElement,
to be able to make things work very well.
Let's go over them.
GeneratesRootInput is something that you
put on your custom annotation.
Specifically, it helps Hilt wait until your annotation processor
finishes generating code, so that then it
can do its discovery mechanism.
Otherwise, if Hilt starts to look for entry points
and modules too early and you haven't generated code,
it'll miss out.
So by annotating your custom annotation
with GeneratesRootInput, you let Hilt know, hey,
I will be generating some modules and entry points.
The second annotation you need to be aware of
is OriginatingElement.
This goes into modules and entry points generated.
It helps Hilt figure out whether the module and entry point
generator goes for a single test or all tests.
In this example, we have an Install Binding
in an asset class of a test and the module
that gets generated in its originating element
has the test class as a reference.
And this basically tells Hilt, hey,
this module that was generated is only for that test.
That's a simple example.
It's on GitHub.
You can take a look at it.
It's really helpful as a reference
for creating an extension.
To recap, to create an extension,
we start by defining a custom annotation.
We add GeneratesRootInput to it.
And then we create an annotation processor
that will generate some module or entry
point based on that custom annotation that we made.
And then, finally, in our code base,
we either use that binding directly
or provide some sort of helper class
to use the generated entry points or modules.
Now, the example, again, that we showed was very simple.
If we start to think about something more realistic,
we can divide it into three patterns, the first one being,
if you have a repeated pattern in your project
where you, every time you create a class,
you create a module for it and you put it
on a multi binding map--
think of it like some sort of clearable or some network
interceptor--
Hilt can help you automate that if you create an extension,
because then you would simply annotate that implementation
with your custom annotation and a module will
be created for it.
So, this can help you with productivity and even possibly
privacy-related things, as it would
make sure things are added and clear if it's
like a multi map of clearables.
The other sort of pattern is supporting member injection
on a custom component.
This is similar to what AndroidEntryPoint does,
where we generate an entry point for your activity
and [? fragment ?] and then we create a subclass that uses it.
So you could technically do the same
with some other UI component or some other non-standard UI
framework, possibly not owned by your code base.
Last but not least, there's this category of custom component
hierarchies.
Sometimes you might want to mirror bindings
from one to the other.
And you can automate that also with that extension, where
it would generate a module for both components
or based on one module, it will generate the other
and so forth.
Yeah.
And that's basically it.
So, hopefully, this is very helpful
and gets you motivated to create an extension.
Maybe publish them open source and others can use it.
We'll see.
Anyway, thank you so much.
[MUSIC PLAYING]