[MUSIC PLAYING] DANIEL SANTIAGO: Hi. My name is Daniel Santiago. And I'm a software
engineer at Google. And in this video, I'll be
talking about Hilt and Android dependency injection library. I'll go a bit into
how to set it up, how does it work internally,
and a quick recap on what is dependency injection. Because we can't have a video
about dependency injection without first
defining what it is, you might have heard of this
already or already know it. It will be a quick recap. And it'll be good
for some follow-up points I'll be making. In essence, dependency injection
is a programming pattern. Here's a concrete example. If we have a music
player, it's a class. It needs a database and
some codecs to work. In a world with no
dependency injection, these dependencies to
make music player work are instantiated by
that class itself, in this case, a music player. To use a music player, it's
pretty straightforward. You instantiate it
and start using it. Now with dependency
injection, these classes that the music
player needs are not built by the player himself. They come as
constructor arguments. And when using the
music player itself, we have to build
those dependencies, then build the music
player and start using it. So it kind of shifts
a little bit the logic of where things get created. Now we actually had a dependency
injection talk in the Android Dev Summit of 2019. Here's a link to that video. And it goes into more
detail on the theory of dependency injection
and the benefits of it. But to basically
recap, we highly recommend that you use
dependency injection in your code base. It does have quite a few
benefits, easier configuration changes. That music player had
an SQLite database. Maybe I want a different
type of database, or I want to add more codecs. It should be easier
on testability. And things are loosely
coupled, and it's easier to reuse code, and
a few other things. We also mention on that talk
that you should definitely use a dependency
injection library, because doing mental injection
can get pretty hairy. It could get a lot-- it
could get a lot harder, because if you have a
bunch of dependencies, then you have to create all of
those in the right order, and so forth. The thing about
dependency injection, or specifically, it's
that it's hard in Android because you don't own
the framework classes. Classes like activity service
and broadcast receiver, you don't instantiate them. The OS will create
those for you and then it will start calling
methods into it. And that's where your
application code takes over. But you don't own
those constructors. So it's not like you
can add parameters to it for your dependencies. There's no way of passing those. There are some factories
that were added in API 28 to work around that. But that's not a realistic
solution, at least not for now. One of the libraries we
recommended on that summit talk was Dagger. And Dagger is great. It's a dependency
injection library that offers compile
time validation of the dependency graph. And it has amazing
runtime performance. But we did notice
that it's hard to use. Especially it's
hard to configure, there's many ways
to configure it. And we saw a handful
of developers that had a hard time
changing that configuration. Specifically if
you still knew how to use-- if you were
familiar with Dagger and you switched to another
app that had Dagger, you would still have a
hard time using Dagger, because the configuration
might be different, or you wouldn't
know exactly what are the subtle changes between
the components and so forth. The thing is that we did a
survey and a lot of developers still asked us for a solution. So what kind of solution
did we have in mind? Well, we thought
of some few goals. And one of them was that we
definitely wanted a little bit something more opinionated,
and especially so that we could make
those choices for you. And because we make choices
for you, things are easier. So something that's
easy to set up. But more importantly,
we want you to focus, within the realm
of dependency injection, to focus back on those
dependencies, the definition of them, and just
using them, and not have to worry about the
wiring of these dependencies. Those were kind of our goals. And this is where Hilt comes in. Hilt it's actually a
dependency injection library that was being used
internally in Google for a bit and we just rename it, change a
few things, and open source it. But Hilt in essence
offers a standard way to do dependency
injection in Android. And what that really means is
all of those different ways to configure
dependency injection change to just being the same. So if you ever work on
an app that has Hilt and then go to another
app, start some other work on some other app that has
Hilt, things will be familiar. So you get that
transfer of knowledge, because things are standardized. Hilt is built on top of Dagger,
so you get those same benefits that Dagger offer,
compile time validation, amazing runtime performance. The solution that we had in
mind is not only about library. We also wanted to
do tooling support. So you can see this with
Android Studio and the Dagger navigation. And because we know
Android is an ecosystem, Hilt also offers extensions
to work with other hard APIs that for DI, mainly, it has
some AndroidX extensions for WorkManager and ViewModels. And we'll show
some of that soon. All right. Let's quickly go through how to
set it up and how things work. So going back to our original
example, we had a music player. This one is simple. It doesn't have anything on its
constructor, not yet at least. To say we want to
make the music player injectable in two places,
we annotate it with @Inject. This is not a Dagger
or Hilt annotation. It's a Java
extension annotation. We have to create an application
annotated with Hilt Android app. This kicks off all of
the injection going on in your application. And then for
activities or fragment, we start annotating them
with AndroidEntryPoint, and this makes Hilt inject
these Android classes. And when Hilt
injects them, it'll inject dependencies that you
have to clear with that inject. So we have a few here
of that music player. And then once
onCreate happens, you are able to use that
player pretty easily. That's it. That's all you need to do
to start getting injection going on in your application. It's pretty easy. To go into a more
real world case, that music player originally
depended on a database. So if we add a database
constructor parameter, we now have to define a way
to provide this database. We're using Room, so
we can't technically just annotate a constructor. Instead, we need another
way to define dependencies. And for that, we have
something we call modules. Modules are a Dagger concept. But because Hilt is
built on top of Dagger, it comes along with that. And modules are
basically a class that defines how
dependencies can be provided. You create a class
annotated with that module. That's the Dagger
annotation installing-- it's a Hilt annotation,
and it tells Hilt where this module will be available. Here, we use
ApplicationComponent. So dependencies
declaring this module will be available all
around the application. The dependency
definition themselves are methods when this
case with provide, another Dagger
annotation, the inputs of this method, the
parameters, will be provided to you by Dagger. And the output is
the return type in this case, a Room database
that we created using the database builder from Room. That's it. With this, the music
player now we'll be able to be created
with our Room database. We saw InstallIn. And InstallIn took a component,
we saw application component. But what exactly are components? Well, I like to also think
about them as DI containers. And I'll show you a bit why. Components are
what Dagger define as that glue that knows how
to create your dependency. If we go back to our
original example, that music player needed a
database and some codecs. That instantiation logic and
the ordering of that, that's basically what a component is. To put it differently,
component is made up of factories that know
how to build stuff. But they also are in
a way a container, because they can hold onto
instances of the things that factory creates. And it's able to reuse
them for other factories or for other parts of your code. And we'll see that in a bit. Hilt comes with a few
predefined components, ApplicationComponent
being one of them. And these match the
Android framework classes. There's an ActivityComponent
and a FragmentComponent. And dependencies flow from
one component to another. They're encapsulated
similarly to how an activity is encapsulated in
an application and so forth. And what this really
means is that data module that we had
with our database, if we had installed that
in our ActivityComponent, activities and fragment
could get injected with that dependency
with the database, but not the
application component. Hilt offers components
for services, too. And we also have ViewComponent,
one with fragment and without fragment. To try to put a different
perspective into this, ApplicationComponent
is managed by the app. So components define the
when and where dependencies are going to be available. And they do that because
they get managed. The components themselves
get managed by the Android equivalent app managing
ApplicationComponent, activities managing each
their ActivityComponent. And because things are
encapsulated in a hierarchy, dependencies in the
ApplicationComponent are accessible to the
ActivityComponent, because they have
a greater lifetime. If we show a practical example,
we inject the music database into the activities, into
these two activities, the ActivityComponent
basically says, hey, ApplicationComponent,
I know you know how to give me a
database, give it to me. And it will create one. But something
interesting here is that we actually get
two different music databases for this activity. And that's not quite right. You kind of want to
share your database, because you want to
share that connection, you want to share
some synchronization. So there are dependencies
that you do want to share and have the same instance. And for that, what we
use is something called scoping and scope annotation. So if we go back
to our data module, we had a method that
provided the database. If we now annotate it with
a scope annotation, this or this one, we use Singleton. Then what Hilt and Dagger
does is they retain that music database across that component. And this is the container part. It can hold onto
that dependency so that if other factories
or other places that need to be injected,
you'll use the same one. So now we use the
same music database for both of our activities. All of the Hilt components
come with scopes. And again, components
define the when and where, the life cycle of
the dependencies. The scope then
defines the retention of those dependencies. They'll get retained for the
lifetime of that component. Now, another concept that
Hilt offers is something called EntryPoint. Entry points are a way of
assessing those components and their dependencies. And these are really useful
for parts of your application that are not supported
by Hilt out of the box. One concrete example
is a content provider, where if you try to Android
Entry Point a content provider, it doesn't work. There's no content provider
component that Hilt offers. They're a bit weird. They have a weird lifecycle. They can be created
before the app. So that's why they're
not supported. But basically, in
the content provider is where do you want
to use a database. So if the content
provider had a way to reach out into the
application component and get that database,
how would it do it, right? And that's where
entry points come in. They are a way of entering
in to your dependency graph. If we have a content provider,
we create an entry point by defining an interface. We annotate it with EntryPoint,
install it in the component where it's going to be
on and get retrieved. This interface has
a getter method. And the return type is
that dependency we want. Now on our actual query
of our content provider, we use utility methods
like EntryPointAssessors to get that entry point out
of the application context. And then from there on, we
can start calling our getters and get those
dependencies that we need. Pretty neat. So entry points are useful for
all of those cases where Hilt doesn't offer
out-of-the-box injection. We mentioned Hilt playing
nicely with the ecosystem is important. So Hilt come with
AndroidX extensions for WorkManager and ViewModel. Work Manager and ViewModel--
workers and ViewModels are interesting. You kind of don't own their
construction injection. They have some
weird instantiation. And Hilt tries to help you
there with these extensions. One example of that is ViewModel
[AUDIO OUT] to create these. You would usually end
up using a factory. But with Hilt, it's as
simple as add injecting-- annotating the constructor
with ViewModelInject. Then you pass your dependency
as usual in the constructor parameter, savedStateHandle,
[INAUDIBLE] assisted. This just means
that that dependency don't have to be defined
in your component. Hilt will know how to create
it for you and give it to you. So it's assisted. And then on any
AndroidEntryPoint that you have, you
can use that ViewModel that you had annotated
previously, either by using the [? coupling ?]
extensions by ViewModels, or simply using the
ViewModel provider. So those are the
new Hilt annotations that we've learned so far. Now the other side of Hilt, or
a really important part of Hilt is the testing side. Hilt offers quite a few
APIs for their testing, which are pretty handy. If we wanted to write a
test for a music player, we create our music player. But in this case, we
don't want to create it with a real database. We want to create it with
an in-memory database. And the reason we
want to do that is because we want
to isolate this test. We don't want to
use a real database. That leaks between tests. We just want a database
that we create here and get rid of pretty quickly. Creating this music
player is pretty easy. We just create that database. But imagine if MusicPlayer
had 20 other [INAUDIBLE]?? And those dependencies
have more dependencies. So what ends up happening is
that you will have to exercise your whole dependency
graph to be able to create an instance of
the thing you want to test. If you have fakes,
you can use those. But then, when you don't
have fakes, you tend to mock. And that by itself has
some other disadvantages. But with Hilt, we do
injection in the test itself. So what happens is the things
that you want to test get constructed the same way they
would for your production [? build, ?] which has the
benefit of you don't have to manage creating all
of those dependencies and [? transmitting ?]
dependencies. So to do that, we start
with HiltAndroidTest. We annotate our tests
with an annotation. We create a rule. And the rule will be
useful for injection later. Similarly to when we
inject into our app, we create a field
with an inject. On our setup, we use the
rule to inject the test. And this is where we
control the injection. And once injection
happens, then you can start using that
instance of the player and start writing
tests and assertions. Now one thing about this test
is it created that music player with the same configuration
we had on our real app. So we will use a real
database, and that's not quite what we want. We wanted an in-memory database. What Hilt offers here
to solve that is Hilt has a way to install modules. And then I can define
a new module that creates the in-memory database. So I uninstall the
data module that was the one used the production
that created the Room database. And then I defined
a new inner class for this test that
creates the in-memory one. The cool thing here is that this
module and this uninstalling only happened for this test. So multiple tests can have
different configurations. So it's pretty flexible. There is some setup required. You do need a runner that
creates an application that's of type HiltTestApplication. And don't forget to
use that same runner when you build the Gradle. To recap, we saw two
annotations here for testing, but there's actually a few
more for other use cases. And I won't go into
details on them. They're a bit special. But we've got documentation on
what kind of issues they solve. Something to keep
in mind with testing is, because you want to
replace dependencies, that means you should
put those dependencies that you want to replace
into their own modules. And since creating
modules is kind of easy, I think this is something
fairly trivial to do, in terms of there's no cost
in creating multiple modules, even if they just have
one dependency that you know you're going to replace. Meanwhile, in a
bigger codebase, you should try to create
smaller test apps, if you have a multi-Gradle
project with many modules for a big app. It's convenient to
create smaller test app to take coverage
in certain areas, because then the scope of
things you need to replace and redeclare are smaller. We know dependence injection
can be core to your code base, and refactoring
that can be tricky. So with that in mind, we do
offer some migration guides. And mainly, if you were already
using Dagger or Dagger Android, Hilt can actually work side
by side, either temporarily-- hopefully temporarily
until you fully migrate. Hilt offers some APIs
that I won't mention here to help with this. We actually have
a whole guide that will give you some
pointers on how to do this. Please check it out. But we will do know changing
the dependency injections, that really can take a toll. All right. So that's Hilt on the basics. Show you how to set
it up, how to use it. But let's take a look
at Hilt in itself and how it works to give
you more sense on some of the decisions that
it makes for you. Hilt is really
composed of two areas. One we like to call the
aggregation part, and the other is the Android
Entry Points part. Let's take a look
at the aggregation. Aggregation here means if you
have multiple modules and entry points, they get aggregated. Let's take a small example here. Say we have an app. This is a Gradle module. And that's composed of a source
set, and source set is classes. You have a class that's a
module and has InstallIn. In a bigger app, you have
other Gradle modules. And all of those
might have modules that also get InstallIn. And there is dependency
between these Gradle modules. Well, Hilt is able to navigate
through these transitive dependencies, and
more importantly, through the classpath
and find those modules and install them on
the right places. In other words, it will
aggregate those modules and entry points
across your classpath. And this has a few benefits. One of them is that
defining a module, if you're working on
a big app and you're working on a small
library part of that app, you can create a module, install
it on a standard component, and you know it'll
be easily accessible. You don't have to go
into the root of your app navigating exactly where
that module should go. It also has practical effects
with variants in flavor. Because they're aggregated
across a classpath, and variants in flavor
affect your classpath. You can have different
configurations between your flavors and
debug and prod variants. One concrete example
of this is say you have an OkHttp
client or a gRPC client. These have interceptors, usually
for logging, authentication, and things like that. If you have a module
that defines that client, and you have no
interceptors on your main, that means when you build
your production app, you won't have any interceptors. But then say on your debug
variant you want to have login, then you can define the module
only on the debug source set. And only when building
the debug variant, that source set becomes
part of the classpath. Hilt discovers that
module, aggregates it. And now ultimately,
you have logging only on your debug OkHttp client. That's pretty neat. Going back into the
aggregation, you might be wondering-- if
you're familiar with Dagger you might be wondering
what exactly is going on. And what's happening is in those
Gradle modules where you have your source set and your
modules and entry points, you're installing
into a component. And these class components
that you're using installing, is nothing more
than a key to tell Hilt, hey, you should create
a Dagger component based on this key. The way you define
these component keys is through a DefineComponent. Because that
DefineComponent tells Hilt to generate a
component, you can also annotate that key with a scope. And that scope will be
added to that component. In essence,
DefineComponent is what Hilt uses to create the
other Android Standard Component, ApplicationComponent,
ActivityComponent, and so forth. And what really
happens is Hilt looks at all those installs
[INAUDIBLE],, aggregates them, and generates a vanilla
Dagger component definition. The modules will go into the
app component module list. And entry points
are just interfaces that get to be implemented
by that Dagger component. Hilt offers this similarly
with sub-components. The find component can actually
[INAUDIBLE] parent attribute. So a FragmentComponent is
based off ActivityComponent. And similarly, ServiceComponent
is a sub-component of ApplicationComponent. And that's how you
form that header key within your component. The standard components
that Hilt offer are fundamental to
Hilt. They really offer that simplicity
of you know where the component is going
to [INAUDIBLE] in its lifetime. You have that knowledge transfer
from one app that uses Hilt to another that uses Hilt.
It really is the building block for other extensions. The AndroidX
extensions, all they do is create more entry
points and modules. And they get installed
[INAUDIBLE] components. And they're key to the
simplicity of Hilt, because they get standardized. Now you could define a
component hierarchy yourself. But then you lose on the
managing of those components. And this is the second
part about Hilt. Hilt not only offers that
aggregation mechanism and has the standard
components defined, it also just offers a way
to use those components via the Android Entry Point. And what Android Entry
Point, all it does is really generate a base
class that knows how to member inject your concrete class. We changed your superclass via
some [INAUDIBLE] transform. That's pretty neat. But ultimately, all
it does is member into your concrete class. And the way does that, because
Android Entry Point also generates a vanilla entry point
that has the member injection method that goes in
the Dagger component and that Dagger will generate
an implementation for. After all, Android Entry
Point is not that magical. And you could define your
own component header key. But then you will have to
manage those component yourself. And similarly to
Android Entry Point, you will have to do the
memory injection yourself. So with that, we learn a bit
about Hilt, how to set it up, some internal semi-complicated
stuff, I guess. But what's next? Well, Hilt is in alpha. And there's still more
work to do, bugs to squash. There's also features we
want to keep working on. We want to be able to allow you
to use Hilt outside of Android Gradle modules. And we want to invest in other
areas, and not just on Hilt, but in Dagger. Being Hilt built
on top of Dagger, we want to invest on Dagger. And we want to provide assisted
injection out of the box. And expanding outside
of the Dagger and Hilt, but still within the realm
of dependency injection, we do want to provide more
AndroidX integration, possibly navigation. And we're thinking far ahead
with things like [INAUDIBLE] and things like that. That's about it. Please try it out. Give us some feedback. Here are some links
to more documentation. Dagger.devhilt goes in
depth on a lot of the APIs. Dagger and Hilt are public
open source on GitHub. You can look at the source
code, file issues there, or questions. And we also have documentation
on d.android.com. These are more story-driven
and use case documentation that are great for more newer
beginners to the subject. That's about it. Thank you so much. Bye. [MUSIC PLAYING]