Stop Holding Dagger by the Blade: Use the Hilt

dagger-hilt-0

Hilt is an Android library that reduces stress levels and stabilizes the blood pressure while using Dagger. If you’ve used Dagger before, we can agree that it has a steep learning curve, a long setup process and often hardly understandable errors.

In some ways, Hilt is a remedy for all three problems.

Getting ready to migrate from Dagger to Hilt

It’s developed by Google and at the time of writing this blog post, it’s still in the alpha stage. So please check for the latest version before using it in production. It looks very promising! Judging by the official documentation page already being full of examples, we can expect a powerful and stable tool soon. Make sure to check the official documentation.

Hilt uses annotations to generate some parts of the dependency structure that you would normally have to write yourself. For example: @HiltAndroidApp will generate ApplicationComponent, ApplicationModule, ActivityBuilderComponent etc. This annotation is a starter pack solution for Dagger setup.

Since the topic of Hilt has already been covered in the documentation, this post will focus on migrating an example project from Dagger to Hilt, so I will assume that you are already familiar with Dagger.

Note this before we begin: It’s highly recommended that you do the migration all at once. Having both Hilt and Dagger (the old way) in your app will result in large overhead as Dagger and Hilt generate their dependency graphs separately. Also, each of your modules must be included in some Hilt component, or else the application won’t build.

Step 1: Add HiltAndroidApp annotation

The application component is usually the entry point for Dagger dependency graph. Dependencies that are included here will be available through the whole application. This is an example of an application setup with Dagger.

	// Application Component class
@Component(
   modules = [
       AndroidSupportInjectionModule::class,
       DatabaseModule::class,
       FragmentBuilderModule::class,
       ActivityBuilderModule::class
   ]
)
@Singleton
interface AppComponent: AndroidInjector<ExampleApplication> {

   @Component.Builder
   interface Builder {
       @BindsInstance
       fun application(application: ExampleApplication): Builder

       fun build(): AppComponent
   }
}

...
// Application class
class ExampleApplication : DaggerApplication() {
   override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
       return DaggerAppComponent.builder().application(this).build()
   }
}

Along with the Application component, it is common practice to also make an Application context module and Fragment and Activity builder modules for your application.

	@Module
class ApplicationModule {

   @Provides
   @Singleton
   fun provideApplicationContext(application: ExampleApplication): Context   
       = application.applicationContext


@Module
interface FragmentBuilderModule {

   /**
    * Add bind function for every fragment
    * with @ContributesAndroidInjector annotation
    * so that the fragment can be injected with dependencies
    */
   @ContributesAndroidInjector(modules = [FriendsModule::class])
   fun bindFriendsFragment(): FriendsFragment
}

Then you would make a module with specific dependencies for every fragment or activity and so on.

What if I told you that all of this boilerplate code I’ve just written can be replaced with just one annotation?

Well that’s exactly what you can get by using Hilt. All you need to do is to place @HiltAndroidApp annotation above your application class and Hilt will do all the magic for you.

	@HiltAndroidApp
class ExampleApplication: Application()

Adding this annotation here will generate:

  • ApplicationComponent
  • ApplicationContextModule – It is included in the ApplicationComponent and it provides context of the application anywhere in the app through the predefined @ApplicationContext annotation.
  • Components for each of your application’s entry points: ActivityBuilderComponent, FragmentBuilderComponent, ServiceBuilderComponent, ViewBuilderModule etc.

Step 2: Annotate your entry points with @AndroidEntryPoint

You might have noticed that you don’t have to use the @ContributesAndroidInjector annotations anymore. So how does the Hilt generated dependency graph know about your entry points? The answer is – @AndroidEntryPoint.

	@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

   @Inject
   lateinit var analytics: AnalyticsProvider

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       ...
   }
}

Adding this annotation above any of the entry points will automatically add them to builder components generated by @HiltAndroidApp, making them injection ready. Hilt will also provide a predefined @ActivityContext annotation to inject activity context anywhere you need.

Everything available in the main application graph can now be injected to your entry points. If you need some specific dependencies for your entry point, you need to make a component that will only be available for that entry point. However, including your modules in ApplicationComponent or any of the generated components will work just as well.

@AndroidEntryPoint can be placed above:

Activity

Fragment

View

Service

BroadcastReceiver

Step 3: Include your modules in the dependency graph with @InstallIn

Almost every project will have some modules that will provide some dependencies, such as network clients or databases. You can keep your existing modules. All you need to do is to add an @InstallIn annotation to each one.

	@Module
@InstallIn(ApplicationComponent::class)
class DatabaseModule {

   @Provides
   fun database(): MyDatabase {
       return MyDatabaseBuilder.build()
   }
}

The@InstallIn annotation takes the component class as a parameter and you can add any of the generated components and even your own component in case you need to restrict access for some dependency. This must be done for every module, otherwise your app won’t build.

@InstallIn can also be used with @EntryPoint annotated classes. While @AndroidEntryPoint will automatically add your class to globally available components, with @EntryPoint you can define the component it will be installed in. This is a way of restricting some dependencies to specific screens or services.

Step 4 (Optional): Integrate Hilt with ViewModels

We have already managed to reduce the boilerplate code amount significantly, but there is more of what you can do with it. Hilt offers a handy solution for ViewModel injection in your android apps. Firstly you need to add some additional dependencies to your app-level build.gradle file.

	implementation ’androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01’
kapt ’androidx.hilt:hilt-compiler:1.0.0-alpha01’
 

Then you need to add @ViewModelInject to your ViewModel.

	class FriendsViewModel @ViewModelInject constructor(
   private val repository: MyRepository
): ViewModel {

   fun getFriendsList() {
       repository.getFriends()
       ...
   }
}

And you are all set. Hilt will even go a step further and it will override the default ViewModelProvider factory with the injected ViewModel ‘s so you can use the viewModels property extension to get your ViewModel ‘s.

	@AndroidEntryPoint
class FriendsFragment : Fragment(R.layout.fragment_friends) {

   val viewModel by viewModels<FriendsViewModel>()

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
       super.onViewCreated(view, savedInstanceState)
       viewModel.getFriendsList()
   }
}
 

A promising future

Hopefully, by going through the steps you can reduce the setup time for dependency injection in your application and make it generally more readable. If you decide to migrate your application to Hilt, I would like to encourage you to keep watching for new updates and changes to Hilt. It is a promising library and plugin, and it could be bringing even more benefits in the future.