[MUSIC PLAYING]
MANUEL VICENTE VIVO: Hi, everyone.
Welcome to the first episode of the Hilt MAD Skills series.
This first one is an introduction
to dependency injection, or VI for short,
on Hilt, Jetpack's recommended solution for DI100.
By following the principles of dependency injection
in your Android app, you lay the groundwork
for a good app architecture.
It helps with reusability of code, ease of refactoring,
and ease of testing.
With dependency injection, instead of
car creating its own instance of engine,
car receives an engine, for example,
as a parameter in its constructor.
This makes the car class reusable,
as it can be used with different implementations of engine,
and it's also easier to test.
The dependency graph of your application
can be represented as using the screen with classes as nodes,
and arrows that indicate the dependencies between classes.
When creating instances of classes in your project,
you can exercise the dependency graph manually
by satisfying the dependencies on transitive dependencies
that the class requires.
But doing this manually every time
involves some boilerplate code, and could be a real problem.
See for example, one of the ViewModels
that we have in iosched, the open source Google IO app.
Can you imagine the amount of code required
to create a FeedViewModel with its dependencies and transitive
dependencies?
It is hard, repetitive, and we could easily
get the dependencies wrong.
By using a dependency injection library,
we can get the benefits of DI without having
to provide the dependencies manually,
as the library generates all the necessary code for you.
And here is where Hilt comes into play.
Hilt is a dependency injection library
developed by Google that helps you get the most out of DI
best practices by doing the hard work,
and generating all the boilerplates you
would definitely need to write otherwise.
By using annotations, Hilt generates that code for you
at compile time, making it really fast at runtime.
This is done using the power of Dagger, a JVM DI library
that Hilt is built on top of.
Hilt is Jetpack's recommended DI solution for Android apps,
and it comes with tooling and other Jetpack library support.
Let's see how do we get started with Hilt in your app.
All apps that use Hilt must contain an application
class that is annotated with @HiltAndroidApp,
since this triggers Hilt's code generation at compile time.
For Hilt to be able to inject dependencies into an activity,
the activity needs to be annotated
with @AndroidEntryPoint.
To inject a dependency, annotate the variables
you want Hilt to inject with @Inject.
All Hilt-injected variables will be available
when super.onCreate is called.
In this example, we are injecting a MusicPlayer
into PlayActivity.
But how does Hilt know how to provide
instances of type MusicPlayer?
Well, it doesn't, at the moment.
We need to let Hilt know how to do it,
using annotations, of course.
Annotating the constructor of a class with @Inject
tells Hilt how to create instances of that class.
This is what's needed to get a dependency injected
into an activity.
We started with a simple example,
as MusicPlayer doesn't depend on any other type.
But if we had other dependencies passed as a parameter,
Hilt would take care of that and satisfy those dependencies
when providing an instance of MusicPlayer.
This was, in fact, a very simple and naive example.
But if you had to do what we've done so far manually, how would
you do that?
When doing DI manually, you can have dependency container
classes that are responsible for providing types and managing
the lifecycle of the instances that it provides.
Oversimplifying a bit here, that's
what Hilt does under the hood.
When you annotate the activity with @AndroidEntryPoint,
a dependency container is automatically created, managed,
and associated to PlayActivity.
Let's call our manual implementation of it
PlayActivityContainer.
By annotating MusicPlayer with @Inject,
we are basically telling the container
how to provide instances of type MusicPlayer.
And in the activity, we would need
to create an instance of the container
and populate the dependencies of the activity using it.
That's also done by Hilt when annotating the activity
with @AndroidEntryPoint.
So far, we've seen that when @Inject
is used to annotate the construction of a class,
it tells Hilt how to provide instances of that class.
And when it annotates a variable in an @AndroidEntryPoint
annotated class, Hilt injects an instance of that type
into the class.
@AndroidEntryPoint that can annotate most Android framework
classes, not only activities, creates an instance
of a dependency container for that class,
and populates all @Inject annotated variables.
@HiltAndroidApp annotates the application class,
and apart from triggering Hilt's code generation,
it also creates a dependency container associated
with the application class.
Now that we got the basics of Hilt covered,
let's complicate the example.
Now, MusicPlayer takes a dependency in its constructor,
MusicDatabase.
Therefore, we need to tell Hilt how to provide
instances of MusicDatabase.
When the type is an interface, or you don't own the class,
because it comes from a library, for example,
you cannot annotate its constructor with @Inject.
Let's imagine that we are using a Room as the persistence
library in our app.
Back to our manual implementation
of PlayActivityContainer, when providing MusicDatabase
that, with Room, this would be an abstract class,
we would like to run some code when
providing the dependency-- in this case, the code
you see on the screen.
Then, when providing an instance of MusicPlayer,
we need to call the method that provides or satisfies
the MusicDatabase dependency.
We don't need to worry about that in Hilt,
since it works up all transitive dependencies automatically.
However, we need to let it know how to provide instances
of type MusicDatabase.
For that, we use Hilt modules.
A Hilt module is a class annotated with @Module,
and within the class, we can have
functions that tell Hilt how to provide
instances of certain types.
This information, known by Hilt, is also
called binding in Hilt jargon.
This function that is annotated with @Provides
tells Hilt how to provide instances of the MusicDatabase
type.
The body contains the block of code
that Hilt needs to execute, which
is exactly the same as we had in our manual implementation
of PlayActivityContainer.
The return type MusicDatabase informs Hilt about what type
this function provides, and the function parameters
tell Hilt the dependencies of the corresponding type--
in this case, the application context
that is already available in Hilt. This code informs Hilt
about how to provide instances of the MusicDatabase type,
or in other words, we have a binding for MusicDatabase Hilt
modules are also annotated with the @InstallIn
annotation that indicates in which dependency containers
or components this information is available.
But what is a component?
Let's cover this in more detail.
A component is a class that Hilt generates
that is responsible for providing instances of types,
like the container we've been programming manually.
At compile time, Hilt reverses the dependency graph
of your application and generates code
to provide all types with their transitive dependencies.
Hilt generates a component or dependency container
for most Android framework classes.
The information, or bindings, of each component
propagates through the components hierarchy
If the music database binding is available
in the SingletonComponent that corresponds to the application
class, it will also be available in the rest of components.
In Hilt jargon, you could say that ActivityRetainedComponent
and ServiceComponents are super-components
of singleton components.
These components are automatically generated by Hilt
at compile time, and they are created, managed,
and associated with the corresponding Android framework
class when you annotate those classes
with @AndroidEntryPoint.
The @InstallIn annotation for modules
is useful to control where those bindings are available,
and what other bindings they can use.
OK, back to our manually created PlayActivityContainer code,
I'm not sure if you realized, but every time
that the MusicDatabase dependency is required,
we are creating a different instance of it.
That's not ideal, since we might want
to use the same instance of MusicDatabase
throughout the whole app.
Instead of a function, we could share the same instance
by having all that code in a variable.
Basically, we are scoping the MusicDatabase type
to this class, as we are always providing the same instance.
How to do this with Hilt?
Well, no surprise here-- with another annotation.
You guessed it.
By using the @Singleton notation in the @Provides method,
we are telling Hilt to always use the same instance
of this type in that component.
@Singleton is a scope annotation.
And its Hilt component has an associated scope annotation.
If you want to scope a type to the ActivityComponent,
you would use the @ActivityScoped annotation.
These annotations can be used in modules,
but they can also annotate classes whose constructor
is annotated with @Inject.
With this, there are two types of bindings.
Bindings that are not annotated with a scope annotation
are called unscoped bindings, like MusicPlayer.
And these bindings are available to all components
if they are not installed in a module.
Scoped bindings that are annotated with a scope
annotation, like MusicDatabase, or unscoped bindings
that are installed in a module, are
available in the corresponding component,
and the ones below it in the components hierarchy.
Hilt also offers integrations with the most popular Jetpack
libraries--
ViewModel, Navigation, Compose, and WorkManager.
Apart from ViewModel, its integration
requires a different library to add to your project.
Check the documentation for more information about it.
Do you remember the FeedViewModel code from iosched
that we saw at the beginning of the episode?
Did you want to see how it looks with Hilt support?
There we go.
Apart from annotating the constructor with @Inject
to let Hilt know how to provide instances of this ViewModel,
we need to annotate the class with @HiltViewModel.
That's it.
You don't manually need to create a ViewModel provider
for this.
Hilt will take care of that.
Curious to see how to get this ViewModel from the fragment?
Here it is, as if you were introducing Hilt. Using
by ViewModels is enough if the activity of fragment
is annotated with @AndroidEntryPoint.
In Compose, it's mostly the same.
You can use the standard ViewModel
function available in Compose.
As I mentioned earlier, Hilt is built
on top of another popular dependency injection
library, Dagger.
Dagger will be often mentioned in the next episodes.
If you are using Dagger, Dagger and Hilt
can work together, with more about the migration
APIs in the guide linked in the description below.
For more information about Hilt, we
have a cheat sheet with the most popular annotations,
what they do, and how to use them.
Apart from our docs on Hilt, we also
have Codelabs to learn with a more hands on experience,
and open source apps that use Hilt.
And that's it for this episode.
Thank you for making it until the end.
I hope you learnt a lot about dependency injection and Hilt.
But this doesn't end here.
We have more MAD Skills episodes coming up,
so please subscribe to the Android Developers YouTube
channel to get notified when they are posted.
Bye.
[MUSIC PLAYING]