Stay up to date with
news on business
and innovation
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Hilt — A New Android Dependency Injection Framework

Krzysztof Król
Android Developer
Technology


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.

Setup

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.

Source: Android documentation

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.


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!

You may also like