Hilt – Android Dependency Injection

11 Likes comments off

[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.

  How to move from Android to iPhone

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

  How To Shoot CINEMATIC VIDEO with Smartphone!

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.

  Best practices for using text in Android

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]

 

 

You might like

About the Author: admin

You cannot copy content of this page