Say Goodbye to Dagger, Hello Hilt

Moti Bartov
The Startup
Published in
8 min readJan 9, 2021
Photo by Ricardo Cruz on Unsplash

In modern SW development and especially in mobile and Android, Dependency Injection is one of the most important topics out there!
Managing and working correctly with dependencies will make our code maintainable and testable.

DI frameworks takes care of creation and providing dependencies which allow us to focus on the important stuff of our code.

When speaking on DI in Android, Dagger and Dagger2 used to be one of the most popular DI frameworks out there, but if you know Dagger, it also required some maintenance and was accompanied with some boilerplate code just for creating the dependencies, and when we wanted to inject dependencies to Android classes like Activity and Fragments, we had to get an instance of DaggerComponent, and run an inject method from it. Or, we could use the Dagger Android extension package for making this a little bit more comfortable.

Still, many developers which I know, suffered when it comes to maintain Dagger code and many times did mistakes which caused build problems or just used it in the wrong way.

Fortunately, Google came out with a new and much more friendly DI framework which built on top of Dagger, but with less boilerplate and much easier to understand. Hilt!

Hilt Building Blocks

  • @HiltAndroidApp — Adds our Application to the Hilt graph
  • @Component — A predefined containers for Android classes scopes
  • @Module — A class, this is where we define provide/bind methods for injecting third party classes objects, or any other types which we don’t have access to their constructor like interfaces
  • @AndroidEntryPoint — Adds Android classes to Hilt graph
  • @ViewModelInject — Used to inject dependencies to ViewModel (say goodbye to ViewModelFactory)

Injection Types
In Hilt, there are 3 ways we can provide dependencies to classes:

  • Constructor injection: Mark the class constructor with @Inject annotation
  • Field injection: Mark a field with @Inject annotation (Basically used in Android classes like Fragment or Activity)
  • Module Injection: Use @Provide or @Bind methods in a @Module

Constructor Injection
So, we’ve created some classes with some dependencies. In order to add these classes to Hilt graph, all we have to do is to mark their constructor with @Inject annotation.

In the above example, both the Car and Engine classes constructors marked with @Inject. Thats all, they are now both added to Hilt graph and will be available in places that needed. Thanks to the @Inject annotation, Hilt knows how to supply the Engine to the Car, and the Car will be available for uses in other places.

Please notice that fields in constructor inject can be private

Field Injection
Usually, Field Injection required in Android classes like Fragment or Activity. The reason for that is that we don’t own these classes, and usually we don’t have access to their constructor and that their creation and lifecycle managed by the Android framework for us.
Suppose, we want to use the Car from the above example in an Android class like a Fragment. For this, we need to do three things:

  1. Mark our Android application with @HiltAndroidApp
  2. Mark our Fragment with @AndroidEntryPoint
  3. Mark the Car field in the Fragment with @Inject

Thats All!! No need to use any DaggerComponent.inject() methods!
Thanks to the @HiltAndroidApp and @AndroidEntryPoint annotations, Hilt can inject dependencies to our Fragment! Awesome!!

Please notice that when we mark a Fragment with @AndroidEntryPoint, we must mark its Activity as well!! Don't forget that! Also, fields marked with @Inject must not be privateIn addition, so far, we haven't used any @Module yet, that's because we've marked all our dependencies constructors (Car and Engine) with @Inject

ViewModel Injection
Suppose we want to inject our Car into a ViewModel, and use this ViewModel in a Fragment. For this, we need to do two things:

  1. Mark our ViewModel constructor with @ViewModelInject
  2. Use the by viewModels syntax in our Fragment

Check this out:

Thats All!!
As we can see, we don’t need to use ViewModel.Factory and ViewModel.Providers() anymore, Hilt will create the ViewModel with the dependency and inject it to our Fragment!

Module Injection
Most of the times, we use third party libraries like Retrofit ,OkHttpClient, SharedPreferences etc’, or, we define our dependencies as an Interface like Repository or DataSource. All above has one thing in common, we don’t have an access to their constructor for annotating them with @Inject! to provide these kind of dependencies we should use a @Module.

Suppose we want to create a network client with Retrofit. Retrofit, depends on an OkHttpClient to work. Both Retrofit and OkHttpClient are third party classes. To create them, we will use a @Module.

  1. Define a Module class and mark it with @Module annotation
  2. Use the @InstallIn to tell Hilt on which predefined component it should be lived! Hilt provides us predefined components which dependencies are scoped in. To learn more about Hilt predefined components, checkout the documentation
  3. Define @Provide methods in the @Module class to create the dependencies

In the above example we defined a the NetworkModule class.
First, we’ve created a class annotated with @Module which installed in the predefined ApplicationComponent, this component lives for the whole period of our app instance. Singleton objects created in this scope available for all of our app runtime.
Then, we defined a method to create the OkHttpClient object with all of its dependencies like interceptors etc’, this one required further down to create the Retrofit object.
Next, We defined another @Provide method to create Retrofit object for the rest of our app. The OkHttpClient required for creating Retrofit will be provided by Hilt thanks to the method above it.
Thats All! Our app Retrofit is ready to use.
We can now use it to create an Endpoint object, this will be done in another @Module for connivance.

In the above example, we defined the Endpoints @Module, here we defined another @Provide method to create the AppEndpoint with the Retrofit object we defined in the NetworkModule. Great!! We have our network infrastructure ready!
We can now use it to create the rest of our architecture dependencies such as Repository and DataSources!

Injecting interfaces using @Bind in a @Module
When defining dependencies, it is highly recommended to define them as instance of an interface. This way, we depend on the interface rather on a concrete implementation. This will gives us more flexibility if we want to replace the logic in the future or supply a mock instance on testing.
As we know, an interface doesn’t has a constructor for us to mark it with @Inject annotation. So, in order to provide dependencies which are an instance of an interface we should use @Bind methods in a @Module.
If this sounds complicated, don’t worry, let see some code examples and it will be much easier to understand.

Suppose our ViewModel needs a Repository to work.
We will define the Repository as an interface first, and then we will create the Repository implementation:

In the above example, we’ve created the Repository interface and the RepositoryImpl class. Notice that the implementation has a dependency also, the RemoteDataSource, which is also an interface!
Lets see it’s code:

In the above code, we’ve defined the RemoteDataSource interface and the RemoteDataSourceImpl class. Notice that the RemoteDataSourceImpl class has the AppEndpoint dependency!
Now, we want to use the Repository in our ViewModel like this:

Although we marked the ViewModel constructor with @InjectViewModel as we should, Hilt cannot tell how to provide the Repository instance because it’s an interface.
Also, it cannot tell how to provide the RemoteDataSource to the Repository implementation as it also, an interface.
To add Hilt all of these we should do the following:

  1. Mark the Impl classes constructor with @Inject
  2. Define @Bind methods for the implementations in a @Module

As said, first, lets mark the implementation classes (RemoteDataSourceImpl and RepositoryImpl) constructors with @Inject

Done! Notice that we already defined the AppEndpoint dependency in the EndpointModule class, this is how Hilt will know how to supply it to the RemoteDataSourceImpl class.

We still need to create another @Module with @Bind methods to provide the RepositoryImpl and RemoteDataSourceImpl classes.

In the above code snippet, we’ve created a new @Module and define two @Bind methods signatures with a single parameter. The method returns type is the interface type, while the parameter type is the implementation!!
Notice, that the @Bind methods are abstract, this is required. Due to that, we should make the whole @Module class abstract as well.

Thats All!! Hilt now has all what it needs to provide interfaces implementations to our ViewModel, thanks to the @Bind methods and our Hilt graph is now ready for work!

Using Qualifiers for multiple instances of the same type
Many times, we need to provide different instances of same type in different places.
For example, we want to have two OkHttpClients with different configuration.
Check out this example:

In the code above, we create 2 network adapters with 2 OkHttpClients. One with interceptor and another one without. Notice that both of the Retrofit provide methods requires OkHttpClient, so how we can distinguish between the two of them?
For this, we should define Qualifiers and use them in the @Module class.

Define Qualifiers for OkHttpClient

In the above code snippet, we’ve defined 2 Qualifiers for both auth and none auth OkHttpClient. Now we can use these Qualifiers in our Module to distinguish between the 2 OkHttpClients objects.

Great!! Qualifiers gives us the power to inject any type which has different instances easily!!

Having @Bind and @Provide methods in the same @Module
There one important thing I like to point out here regarding the abstract @Bind methods in an abstract @Module. As I mentioned in the above sections, @Provide methods and @Bind methods defined in a @Module class, but @Bind methods are abstract and @Provide methods aren’t. As a result, when we want to use @Bind methods we must make our @Module class abstract as well. This causes a little problem as we cannot have both abstract and non-abstract methods in an abstract @Module class.

However, there is a solution for this situation. Simply, by making our @Provide methods static (in Kotlin we use the companion object) we can have both @Bind and @Provide methods in the same module.

Here is an example:

In the example above, we can see how having both @Bind and @Provide methods in the same abstract @Module

Summary
As I am already using hilt in my Android app for a few weeks, I’ve found it highly simpler than Dagger! it makes my project cleaner and much more maintainable than ever! Hilt provides much more options and capabilities when it comes to scope objects in the lifecycle of our app, you can read about all the options in the details documentation:
In addition, I encourage you to go over the Hilt CodeLab which will give you first HandsOn experience and also will give you a little taste of to work with Hilt and testing.

--

--