About The Series
This article is part of the following series:
PART 1: Getting To Know
Here I talk little about Hilt, just how to setup Hilt for simple project and inject simple dependency intoMainActivity
.PART 2: Basic Concepts (This Article)
Here I talk a bit more about Hilt concepts like Hilt Bindings, Field Injection, Hilt Modules, Hilt Components, and more.
Introduction
In this article, we will learn some good concepts of Hilt that will actually make you start using it in your project.
We will learn following concepts:
So let's begin.
Inject Dependencies
Basically you have a class, that's required by an activity, a fragment or any other class in your project. You need to obtain an instance of this class from Hilt. To do so, you want to apply the following:
Define AndroidEntryPoint
Annotate your Android class (Activity, Fragment, Service, etc..) with@AndroidEntryPoint
.
Hilt provides any required instances to Android classes that have theAndroidEntryPoint
annotation.Perform Field-Injection
Annotate the required dependency with@Inject
.
This makes Hilt provide an instance of this dependency to your Android class.Define Hilt Binding
Annotate the constructor of that dependency class with@Inject
.
This defines a Hilt Binding that makes Hilt know the necessary information to provide instances of this type as a dependency.
NOTE: If you annotate an Android class with
@AndroidEntryPoint
, then you also must annotate Android classes that depend on it. For example, if you annotate a fragment, then you must also annotate any activities where you use that fragment.
Let see an example in which we have MainActivity
class that requires NamesAdapter
as dependency.
Here we define a Hilt binding for NamesAdapter
by annotating the constructor with @Inject
:
public class NamesAdapter extends ... {
@Inject
public NamesAdapter() {
...
}
...
}
Then we annotate MainActivity
with @AndroidEntryPoint
, and its dependency with @Inject
:
NOTE: Hilt cannot provide dependencies for private fields.
@AndroidEntryPoint
public class MainActivity extends ... {
@Inject
NamesAdapter mNamesAdapter;
...
}
Now Hilt learns that MainActivity
requires a dependency of type NamesAdapter
, and knows enough of how to provide an instance of this dependency.
That's it for how to basically Inject Dependencies in Hilt.
Hilt Modules
For NamesAdapter
class we actually have full control of it, we created it, thus we can define Hilt Binding using constructor injection. What about dependencies that we don't own? like a sub-classing Room class? Well, this is managed by Hilt Modules.
Hilt modules are classes we define that are used to add bindings to Hilt.
In Hilt modules, you include bindings for types that cannot be constructor injected such as interfaces or classes that are not contained in your project (ex: OkHttpClient
class).
The following module, @Module
tells Hilt this is a module and @InstallIn
tells in which containers the bindings are available by specifying a Hilt Component:
@Module
@InstallIn(ApplicationComponent.class)
public class DatabaseModule {
...
}
When the ApplicationComponent
is generated, all bindings defined in DatabaseModule
will be available for Hilt to provide when required.
That's it for Hilt Module concept.
Hilt Components
A Hilt Component is made by of factories that know how to build stuff. It defines the lifecycle and scope of a dependency.
You can also think of a Hilt Component as a container, because you can hold onto instances of the things that factory creates.
Hilt components are encapsulated in a Component Hierarchy, which means dependencies included in upper level component will be accessible to lower level components since upper level component has a greater lifetime.
For each Android class in which you can perform field injection, there's an associated Hilt Component. For example: ApplicationComponent
is managed by Application
, ActivityComponent
is managed by Activity
, and so on (Learn more about each Component Lifetime here).
The following module provides an instance of AppDatabase
class which will be included in ApplicationComponent
container:
@Module
@InstallIn(ApplicationComponent.class)
public class DatabaseModule {
@Provides
public AppDatabase provideDatabase(@ApplicationContext Context context) {
Room
.dataBuilder(context, AppDatabase.class, "app")
.build();
}
}
As you notice, provideDatabase()
method is annotated with @Provides
which means provideDatabase()
method will be executed every time Hilt needs to provide an instance of AppDatabase
class.
But provideDatabase()
method still depends on Context
, so?! Well, each Hilt container comes with a set of default bindings that can be injected as dependencies into your custom bindings, so in this example the annotation @ApplicationContext
will make Hilt provide you with an instance of ApplicationContext
.
That's it for Hilt Component concept.
Component Scopes
In real world you definitely need one single instance of AppDatabase
database, not a new instance each time some class requires it. Well, this is managed by Component Scopes.
By default, all bindings in Hilt are not scoped. This means that each time there is a request for the binding, Hilt creates a new instance of the needed type.
What we mean by scope a binding to the component, is that Hilt will create a scoped binding once per instance of that component. For example, if you have a binding scoped to ActivityCompoent
and your app has two activities, Hilt will create a scoped binding for each activity.
A scoped binding shares the same instance for all requests. So to provide a single instance of Room
database, you just need to make the binding providing this instance of Room
class, scoped to the component.
To scope a binding to a component, we use scope annotation that's available for each generated component. For example, for ApplicationComponent
we use @Singleton
and for Activity
we use @ActivityScoped
(You can find other annotations here).
In the following module, we define a binding for AppDatabase
class that is scoped to ApplicationComponent
by annotating the method with @Singleton
:
@Module
@InstallIn(ApplicationComponent.class)
public class DatabaseModule {
@Provides
@Singleton
public AppDatabase provideDatabase(@ApplicationContext Context context) {
Room
.dataBuilder(context, AppDatabase.class, "app.db")
.build();
}
}
Btw, what if you want a constructor injected class like NamesAdapter
to be scoped too? well, just annotate the class with the proper scoping annotation as following:
@ActivityScoped
public class NamesAdapter extends ... {
@Inject
public NamesAdapter() {
...
}
...
}
Before we leave this part, what if we annotated DatabaseModule
with ActivityComponent
instead of ApplicationComponent
:
@Module
@InstallIn(ActivityComponent.class)
public class DatabaseModule {
@Provides
@Singleton
public AppDatabase provideDatabase(...) {
...
}
}
When Hilt encounters activity creation, it will generate a container holding all bindings of DatabaseModule
, and when another different activity creation happens, it will generated different container holding different bindings of DatabaseModule
. Here in this case, different activities means different generated components, and each component will hold its own scoped version of AppDatabase
. So be careful with these concepts, and remember each Hilt Component is managed by the lifecycle of its corresponding Android class for which it is generated. I just wanted to harden this point with you.
That's it for Component Scopes concept.
Integration with ViewModel
Hilt includes extensions for providing classes from other Jetpack libraries. Hilt currently supports the following Jetpack components:
ViewModel
WorkManager
You must add another Hilt dependencies for Hilt to work with ViewModels:
...
dependencies {
...
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
// When using Kotlin.
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
// When using Java.
annotationProcessor 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
}
Now we are ready to use Hilt with ViewModel. Just inject the constructor with @ViewModelInject
:
NOTE: You must also annotate the
SavedStateHandle
dependency with@Assisted
.
public class MainViewModel extends ViewModel {
private final MainRepository repository;
private final SavedStateHandle savedStateHandle;
@ViewModelInject
public MainViewModel(
MainRepository repository,
@Assisted SavedStateHandle savedStateHandle)
) {
this.repository = repository;
this.savedStateHandle = savedStateHandle;
}
}
That's it.
But know that this does not mean Hilt will provide you an instance of MainViewModel
class, @ViewModelInject
is similar to @Inject
, so dependencies defined in the constructor parameters of MainViewModel
will be injected by Hilt.
So in your activity/fragment that's annotated with @AndroidEntryPoint
you can get the MainViewModel
instance as normal using ViewModelProvider
or by the by viewModels()
kotlin extensions:
@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
private MainViewModel mMainViewModel;
@Override
protected void onCreate(Bundle savedInstaneState) {
super.onCreate(savedInstaneState);
mMainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
}
}
And if you wonder if Hilt have another annotation so it can provide an instance of MainViewModel
, you can check this comment from Hilt team:
So @ViewModelInject is kind of a one way thing, similar to @AndroidEntryPoint in a way. It only gets Dagger dependencies into your class. It does not add your ViewModel to the object graph as a Dagger binding. In order to access your ViewModel in a provider or somewhere else, you still need to go through the normal Android APIs of getting a view model like using a ViewModelProvider.
The reason we can't provide it into the graph is that we don't know what ViewModelStoreOwner you want to use it with.
It makes sense, right?
That's for integrating Hilt with ViewModel, and you can check the doc for integrating other parts of Jetpack with Hilt.
Hilt Notes & Warnings
We are almost done in this article, but there are just some notes and warnings to know about Hilt, I didn't want to bother you while reading, so take a quick look on them:
Projects that use both Hilt and data binding require Android Studio 4.0 or higher.
Hilt only supports activities that extend
ComponentActivity
, such asAppCompatActivity
.Hilt only supports fragments that extend
androidx.Fragment
.Hilt does not support retained fragments.
Under the hood, Hilt will populate the required fields of a fragment in the
onAttach()
lifecycle method with instances built in the automatically generatedFragment
's dependencies container.Fields injected by Hilt cannot be private.
Bindings available in containers higher up in the hierarchy, are also available in lower levels of the hierarchy.
Scoping a binding to a component can be costly because the provided object stays in memory until that component is destroyed. Minimize the use of scoped bindings in your application.
More Concepts?...
There is actually more, but we should talk about them in another article. For now, let's have an idea about some:
Entry Point
Hilt currently supports the following Android types:Application
(by using@HiltAndroidApp
),Activity
,Fragment
,View
,Service
andBroadcastReceiver
, so how we inject dependencies for other classes?! Well, we need to define a custom Entry Point.@Binds
If the dependency is an interface, thus we cannot use constructor injection, so how to inject it?! To tell Hilt what implementation to use for an interface, you can use the @Bind annotation on a function inside a Hilt module.Qualifiers
Hmm, well there is a way to inject interface as dependency, but what if we have different implementations and we need to provide these implementations in the same project? Well, to tell Hilt how to provide different implementations (multiple bindings) of the same type, we use qualifiers.
So! That's all for this article. Please leave a comment if you have a question or to add value to this article.
Thank you for reading 🙏.