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 theApplicationComponent
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.