In the brief history of Android development, we’ve used quite a few dependency injection libraries. First came RoboGuice. Ported from Google’s Java DI framework “Guice,” it had some flaws but nevertheless gained a lot of popularity among the Android developer community. Then Square introduced Dagger. Thanks to its precompiler, Dagger doesn’t rely on reflection as much as RoboGuice, which makes it a lot faster and better suited to a mobile environment.
Performance advantage swayed many Android developers to switch.
Following this, the Google team introduced Dagger 2. It brought further performance improvements compared to its predecessor. Dagger 2 generates its dependency graph at compile time, eliminating reflection all together. Today, Google’s library is the framework of choice for the developer community.
Over 50 thousands applications in Google Play use Dagger as their dependency injection solution.
While Dagger 2 is a very fast and robust framework it has some flaws. It’s quite complicated — it has a steep learning curve and requires a lot of boilerplate. To remedy those problems, Google created a new DI solution: Hilt.
Hilt is a library created on top of Dagger 2 and promises to simplify implementation.
Let’s have a look how Hilt accomplishes this goal.
To use Hilt, we need to add proper dependencies to a project.
Let’s add Hilt’s plugin to our project’s root build.gradle file.
Please remember to update the dependencies version to the latest one.
Note: At the moment, Hilt is in alpha version — we don’t recommend using it in a production environment.
Now let’s apply the plugin and add additional dependencies to app/build.gradle file.
Lastly, to use Hilt, we need to enable Java 8 in a project. We will add this snippet to app/build.gradle file.
Now that we are done with the setup, we will check how to integrate Hilt into our project.
If you are familiar with Dagger, then you know that to integrate DI in your application, you need to create suitable components and modules. Then in Android classes like Activity, Fragment, Service, or your custom views you would need some boilerplate to inject your dependencies. Fortunately this integration with Hilt is much simpler.
First, create a custom application class and then properly annotate it.
As you can see, the only thing we need to do is to add @HiltAndroidApp annotation. Once this is done, we need to properly annotate Android classes in which we want to inject some dependencies.
Here, we annotated our MainActivity with @AndroidEntryPoint annotation. Now we are able to inject some fields into our Activity. Before that, let’s create a dependency that we might want to inject.
Now thanks to @Inject annotation, we can inject our FileLogger to MainActivity class.
We’ve completed basic Hilt integration into our application. Now we will talk about the main difference between Hilt and Dagger — their approach to components handling.
Dagger advocated creating separate components and modules for each application screen. While this approach has advantages — like loose coupling or high degree of modularisation — the resulting code would be often complicated and difficult to understand. Hilt, in contrast, introduces a few predefined components and discourages creating new ones.
Let’s take a look at Hilt’s component hierarchy.
All your activities will share one ActivityComponent, all your fragments will share one FragmentComponent, and so on. It means that if a dependency is available in one Activity, then it’s available in all the others. Also, if a dependency is available in one component, it’s also available in all its descendants.
Apart from predefined components, Hilt also offers predefined scopes. If a dependency is annotated with @ActivityScope, it means that Hill will offer the same instance of the dependency under the given activity component.
Let’s look at some code to clarify it.
First, we will define a module that will be installed in ActivityComponent.
If you are familiar with Dagger, then you will notice that Hilt’s approach to modules is also quite different. In Hilt, each module has to be annotated with @InstallIn annotation.
An argument this annotation takes defines in which component the module will be installed in. In Dagger, on the other hand, we would declare modules in the component class.
Once we created our ActivityModule, we can now inject AnalyticsService to our Activity.
Now let’s assume that MainFragment resides in our MainActivity. As FragmentComponent is a descendant of ActivityComponent in Hilt’s component hierarchy, we can use analyticsService dependency there as well.
You might remember that our AnalyticsService was annotated with @ActivityScoped annotation. Because of that, MainActivity and MainFragment will receive the same instance of AnalyticsService.
Note: If we created OtherActivity and injected there AnalyticsService, then the OtherActivity would receive a different instance of AnalyticsService than MainActivity. It would happen this way because MainActivity and OtherActivity don’t share an ActivityComponent instance.
Earlier, when we created our FileLogger class, we forgot about one thing. To be able to use a device's file system, we need to have access to an instance of the Context class.
Fortunately Hilt offers predefined bindings that will solve our issue quite easily. Bindings that we can use vary between components.
Component Predefined bindings
ActivityComponent Application, Activity
FragmentComponent Application, Activity, Fragment
ViewComponent Application, Activity, View
ViewWithFragmentComponent Application, Activity, View, Fragment
ServiceComponent Application, Service
From the table above, it’s clear that in FileLogger we can use Application binding. Application inherits from the Context class, and we can inject application as context to our logger class.
We will use Hilt’s @ApplicationContext annotation to tell Hilt that we want specifically an instance of application context.
Injection into Not Supported Classes
Unfortunately Hilt’s @AndroidEntryPoint right now isn’t supported in all Android components. One of them is the ContentProvider class.
Let’s see how we can get access to our dependencies in this case. According to Hilt’s documentation, we should create a custom entry point to our dependency graph.
We are installing this entry point in ApplicationComponent, which means that we will only have access to dependencies from ApplicationComponent. Another thing we have to do is to list explicitly all dependencies that will be retrieved via our custom entry point.
Once this is done, we can get access to our entry point by EntryPoint.fromApplication(Context context, Class<T> entryPoint) method.
Hilt and View Models
Let’s investigate now how Hilt integrates with ViewModel library. This library is a part of Jetpack — suite of libraries provided by Google that helps developers follow best coding practices.
At nomtek, we commonly use MVVM with Clean Architecture, and the ViewModel library is a great fit for this pattern.
First, we will declare our view model.
To initialize the view model in our ProductDetailFragment, we can use the by viewModels() extension.
Let’s assume now that we want to pass product id to our ProductDetailFragment.
Now we would like to pass product id from fragment to ProductDetailViewModel. This way, in the view model we will be able to retrieve the product from a repository.
The question now is how to pass product id?
There are two possible approaches.
One approach is to pass product id in onViewCreated().
Then in the view model we would initiate product fetching.
There is one major issue with this approach. In case the user changes the device's orientation, a fragment will be recreated and onViewCreated method will be called once again. This is an undesired behavior as view models are preserved on the configuration change and there is no need to fetch the product once again.
To fix this problem, we could add some conditional logic and check if the product wasn’t fetched before. However, there is a better solution to this problem — Saved State module created by Google.
Thanks to this library and Hilt’s ViewModel Jetpack integration, we will be able to receive the SavedStateHandle object in the view model’s constructor. This SavedStateHandle object will contain extras passed to fragment’s arguments, and because of that we can initiate product fetching in the view model’s init block.
Now in case of a configuration change, our product will be fetched just once and everything will be fine.
Take notice of two important annotations here: @ViewModelInject and @Assisted.
@ViewModelInject annotation is similar to @Inject, but it also informs Hilt that the annotated class is a view model and should be treated differently than regular dependency
SavedStateHandle is marked with @Assisted annotation so that Hilt uses assisted injection. Assisted injection is a mechanism that Hilt uses under the hood to inject objects from outside of the dependency graph.
Problems with Hilt
We find Hilt to be a very promising framework, and we would like to start using it in production environments as soon as possible. Unfortunately there are problems connected with it that we ought to mention.
Hopefully this article gave you a good overview on what you can expect from Hilt and how you can use it in your projects. At Nomtek, we’re very excited about the future perspectives of Hilt, and we can’t wait to use it in our projects. Unfortunately as we mentioned earlier, Hilt is not quite ready for production, and we decided to wait for the beta version.
What’s your opinion on Hilt? Do you agree with us? Please, share your thoughts!