Hilt: Setting Up
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.
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
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.
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
dependencies {
implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}
Lastly, to use Hilt, we need to enable Java 8 in a project. We will add this snippet to app/build.gradle file.
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
We’ve completed the basic setup. Now we will add additional dependencies that are needed for the integration of ViewModel and SavedStateHandle libraries. We will need them later on.
// viewModel library
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
//hilt viewModel integration library
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01"
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
// by viewModels() extension for activity and fragment
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
Now that we are done with the setup, we will check how to integrate Hilt into our project.
Hilt Integration
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.
@HiltAndroidApp
class HiltApplication : Application() {
}
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.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...
}
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.
class FileLogger @Inject constructor() {
...
}
Now thanks to @Inject annotation, we can inject our FileLogger to MainActivity class.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var fileLogger: FileLogger
...
}
Simple enough!
Hilt Components
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.
@Module
@InstallIn(ActivityComponent::class)
class ActivityModule {
@ActivityScoped
@Provides
fun provideAnalyticsService() = AnalyticsService()
}
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.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var analyticsService: AnalyticsService
...
}
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.
@AndroidEntryPoint
class MainFragment : Fragment() {
@Inject
lateinit var analyticsService: AnalyticsService
}
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.
Pre-Defined Bindings
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
ApplicationComponent Application
ActivityRetainedComponent Application
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.
class FileLogger @Inject constructor(
@ApplicationContext context: 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.
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface SampleContentProviderEntryPoint {
fun analyticsService(): AnalyticsService
}
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.
class SampleContentProvider: ContentProvider() {
override fun onCreate(): Boolean {
context?.applicationContext?.let {
val clazz = SampleContentProviderEntryPoint::class.java
val hiltEntryPoint =
EntryPointAccessors.fromApplication(it, clazz)
hiltEntryPoint.analyticsService().contentProviderStart()
}
return true
}
...
}
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.
class ProductDetailViewModel @ViewModelInject constructor(
private val logger: FileLogger
) : ViewModel() {
}
To initialize the view model in our ProductDetailFragment, we can use the by viewModels() extension.
@AndroidEntryPoint
class ProductDetailFragment : Fragment() {
private val viewModel: ProductDetailViewModel by viewModels()
...
}
Let’s assume now that we want to pass product id to our ProductDetailFragment.
companion object {
const val PRODUCT_ID = "product_id"
fun newInstance(productId: String) = ProductDetailFragment().apply {
arguments = bundleOf(
PRODUCT_ID to productId
)
}
}
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().
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val userId = arguments?.get(USER_ID) as? String
userId?.let {
viewModel.init(userId)
}
}
Then in the view model we would initiate product fetching.
class ProductDetailViewModel @ViewModelInject constructor(
private val logger: FileLogger
) : ViewModel() {
fun init(userId: String) {
//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.
class ProductDetailViewModel @ViewModelInject constructor(
@Assisted private val savedState: SavedStateHandle,
@LoggerSimple private val logger: Logger
) : ViewModel() {
init {
savedState.get(ProductDetailFragment.USER_ID)?.let {
//fetch product from repository
}
}
}
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.
- Right now Hilt is only in the alpha stage and API is not finalized. Future API changes might require a lot of additional work from our development team and prove to be costly.
- Hilt is not the most stable library right now, and it’s common to encounter weird bugs. For example, at the moment of writing this article, the injection to custom views is broken. Fortunately it can be fixed by changing the JavaPoet library version https://github.com/google/dagger/issues/1909.
- There’s also a potential increase in compilation time. Hilt adds a lot of new features on top of the Dagger library and this might require additional processing. We encourage you to profile your build time if you decide to migrate to the Hilt framework.
Conclusion
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!
Related articles
Supporting companies in becoming category leaders. We deliver full-cycle solutions for businesses of all sizes.