I had been using Dagger for all my projects as the dependency injection solution. It was the go-to library for managing dependencies in your application when developing an application with testability and code separation in mind. The Dagger library was not originally designed for the Android framework. Because of that Dagger was not that easy to work within the Android framework but once you figure out how to use it, it’s the best solution available for android project dependency injection. The Hilt library is the new dependency injection solution for android which replaces the Dagger. The Hilt was built specifically for Android project development. It reduced the significant amount of code that we need to write and it has built-in support for ViewModel
injection.
In this article, I’m using my old project which I used in the MVVM error handling article series. I have expanded that project into an indoor plant selling sample application. This way we have a few screens to work with plus it covers most of the dependency injection scenarios that we face in real-world application development. You can find the code in this repository. Refer to dagger_to_hilt_start branch for the start point of the application. you will be able to see the end result of this implementation on dagger_to_hilt_end branch. In this article, I will mainly focus on explaining how to perform the transition to Hilt from Dagger. Even though I will get into the details of the Hilt library I will include some parts of official documentation in this article because there is no point in rewriting the same details. So let’s clean the project and delete all the files inside “di” folder.
Adding Hilt library to the project
Go to the project Gradle file make add the Hilt Gradle plugin.
buildscript {
ext.hilt_version = '2.40'
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
And make the following changes to the module Gradle file.
plugins {
id 'dagger.hilt.android.plugin'
}
dependencies {
//Dagger
//implementation 'com.google.dagger:dagger:2.30.1'
//implementation 'androidx.legacy:legacy-support-v4:1.0.0'
//kapt 'com.google.dagger:dagger-compiler:2.30.1'
//Assist inject
//compileOnly 'com.squareup.inject:assisted-inject-annotations-dagger2:0.6.0'
//kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.6.0'
// Hilt dependencies
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
That’s all the edits we need to do on the Gradle side. Now sync the project.
Setup Hilt Application level
When using Dagger first step is to define the application component class. In Hilt, we don’t need to implement a separate class. Just need to add @HiltAndroidApp
annotation to the app application class. The class annotated with @HiltAndroidApp
act as the parent component and triggers the dependency generation for the application.
@HiltAndroidApp
class ThisApplication : Application() {
companion object {
val buildType: BuildType = BuildType.RELEASE
}
override fun onCreate() {
super.onCreate()
Stetho.initializeWithDefaults(this)
}
}
UI Level dependency injection
Let’s start from the MainActivity
. In the activity class NavController
instance needed to be provided using dependency injection. Like in Dagger we need to define how to create NavController
in a module. To create NavController
instance for the activity we need to pass the activity instance and the id of the fragment that hosts navigation. In Dagger, we had a factory interface that can be used to define the relevant subcomponent and use it to pass parameters. In Hilt, we don’t need to create subcomponents at all. We can directly create a module and define the instance we need to inject. Every Hilt module should add the @Installin
annotation which use to define what android scops these instances are provided for. In another world, this is the same as subcomponents in Dagger but it is pre-defined in Hilt for all the common use android classes all we need to do is include it in our module. You can find all the available components in here.
There is a simple rule to follow when defining constructing instance to inject in Hilt. If the instance that has to inject is an interface use @Bind
annotation in the module function which creates and returns the relevant instance. If it is a third-party library instance that you don’t have control over or it requires you to use a builder pattern to create the instance of it use @Provides
annotation. So in our case, NavController
instance should be injected and it is a general class that comes from an external library. However, we need to provide the activity and id of the fragment that hosts the navigation graph. In order to achieve this, we need to create an interface that accepts these parameters and a class that implement that interface and creates the instance of the NavController
. ActivityNavigator
interface can be created like this.
interface ActivityNavigator {
fun getNavController(hostFragmentId: Int): NavController
}
Note that this interface function doesn’t require the activity even though we need it to create the instance of the NavControlle
r. It’s not necessary because when a module is created with @InstallIn(ActivityComponent::class)
activity instance is available inside that module. You will get a clear idea when you see the module implementation. But before that, we need to create a class that implements the above interface.
class ActivityNavigatorImplementation @Inject constructor(private val activity: FragmentActivity): ActivityNavigator {
override fun getNavController(hostFragmentId: Int): NavController {
val navHostFragment = activity.supportFragmentManager
.findFragmentById(R.id.hostFragment) as NavHostFragment
return navHostFragment.navController
}
}
Next, a module needs to be created which is scoped into an activity lifecycle like this.
@InstallIn(ActivityComponent::class)
@Module
abstract class ActivityNavigationModule {
@Binds
abstract fun bindNavigator(impl: ActivityNavigatorImplementation): ActivityNavigator
}
Finally, NavController
instance can be injected into the MainActivity
like below.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var navigator: ActivityNavigator
private lateinit var navigationController : NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
initialization()
}
private fun initialization() {
navigationController = navigator.getNavController(R.id.hostFragment)
}
Every android class that Hilt needs to provide instance should be annotated with @AndroidEntryPoint
. After that ActivityNavigator
is injected into the activity. inside the initialization()
host fragment id is passed to the getNavController
function which returns the NavController
instance that was defined inside ActivityNavigatorImplementation
class.
Same as above NavController
can be injected into fragments in the application. I’m not going to explain that part here because it’s very similar to this process. Check out the source code for that. At the UI level ViewModel
classes also need to be injected. But I’ll explain that part after explaining how hilt works on ViewModel
.
ViewModel Level dependency injection
ViewModel
Injection using Dagger was always has been a complicated task. One of the main focuses of Hilt is to make it easier to work with ViewModles
. Let’s take look at how to do dependency injection to a simple ViewModle
which does not require any runtime parameters to construct itself. The Hilt library has pre-define @HiltViewModel
annotation which let Hilt know this class is a ViewModle
and its injected dependencies should attach to that ViewModle
‘s lifecycle.
@HiltViewModel
class LoginViewModel @Inject constructor(val repository: LoginRepository)
: BaseViewModel(repository){
}
After adding that @HiltViewModel
annotation you can simply inject that ViewModle
into UI layer like this.
private val viewModel: ViewModelA by viewModels()
Let’s take a look at how to inject ViewModle
when you have a dynamic parameter to pass into the ViewModle
. When using Dagger assisted injection third party library was needed to achieve runtime parameter injection to ViewModle
. In Hilt, it’s a built-in future that we can use without any additional libraries. let’s take a look at how to do that. In this sample application PlantDetailsViewMode
l require plant id as a parameter.
class PlantDetailsViewModel @AssistedInject constructor(
val repository: PlantDetailsRepository,
@Assisted private val plantId: Int) : BaseViewModel(repository) {
val plantsDetailLiveData: LiveData<PlantDetails>
@AssistedFactory
interface PlantDetailsViewModelFactory{
fun create(plantId: Int): PlantDetailsViewModel
}
companion object {
@Suppress("UNCHECKED_CAST")
fun providesFactory(
assistedFactory: PlantDetailsViewModelFactory,
plantId: Int
): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(plantId) as T
}
}
}
}
In order to inject any parameters to a ViewModel
ViewModelProvider.Factory
interface needs to be implemented. Hilt assisted injection solution is similar to the previous solution in the project. The constructor of the ViewModel
annotated with @AssistedInject
let Hilt know this class will use runtime parameters to create the object of the ViewModel
. Runtime parameter is marked with @Assisted
annotation let Hilt know plantId
is the dynamic parameter. Then PlantDetailsViewModelFactory
is defined with @AssistedFactory
to use inside ViewModelProvider.Factory
which gives us the ability to pass the plant Id from the PlantDetailFragment
.
@AndroidEntryPoint
class PlantDetailFragment : BaseFragment(R.layout.fragment_plant_detail) {
@Inject
lateinit var articlesFeedViewModelFactory: PlantDetailsViewModel.PlantDetailsViewModelFactory
override val viewModel: PlantDetailsViewModel by viewModels {
PlantDetailsViewModel.providesFactory(
assistedFactory = articlesFeedViewModelFactory,
plantId = PlantDetailFragmentArgs.fromBundle(requireArguments()).plantId
)
}
}
In the fragment, you can create the ViewModel
with the help providesFactory
function inside the PlantDetailsViewModel
. Using Hilt PlantDetailsViewModelFactory
interface PlantDetailsViewModel
is injected into the PlantDetailFragment
.
Repository Level dependency injection
Finally, we are in the very bottom layer of the architecture. The repository layer is responsible for handling the main business logic of the application. In this example, repository layers perform network calls that are related to their top-level UIs. When using Dagger ApiService
is injected into the repositories let’s see how to achieve the same thing by using Hilt.
@InstallIn(SingletonComponent::class)
@Module
object NetworkModule {
@Provides
fun provideConnectivityInterceptor(@ApplicationContext context: Context) =
ConnectivityInterceptor(context)
@Provides
fun provideMockInterceptor(sharedPreferences: SharedPreferences) =
MockInterceptor(sharedPreferences)
@Provides
fun provideHttpClient(connectivityInterceptor: ConnectivityInterceptor,
mockInterceptor: MockInterceptor): OkHttpClient.Builder {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(connectivityInterceptor)
.addInterceptor(mockInterceptor)
}
@Provides
fun baseURL() = "https://dummyurl.com"
@Provides
fun provideRetrofit(baseURL: String, httpClient: OkHttpClient.Builder): ApiService {
val retrofit = Retrofit.Builder()
.baseUrl(baseURL)
.client(httpClient.build())
.addConverterFactory(MoshiConverterFactory.create())
.build()
return retrofit.create(ApiService::class.java)
}
}
Just like with Dagger a module needs to be created to construct ApiService
object and pass it into the repository. As you may have recognized this class is very similar to the previous NetworkModule
one on the Dagger implementation. ApiService
object is depending on OkHttpClient
. OkHttpClient
object is depending on ConnectivityInterceptor
and MockInterceptor
which are classes created by us. And Finally MockInterceptor
need SharedPreferences
instance. To provide that another module is needed to be created. I will not add that class here because it is the same as this module. Please check the source code. So we have a bunch of dependencies here that some of them can only be created using the builder pattern and other instances are created by us so we have control over them. So we can have them in an Object
class and the instances can be defined with @Provides
annotation. To create ConnectivityInterceptor
context instance is needed. Hilt provides a pre-defined context instance of application context which can be accessed by @ApplicationContext
and activity context which can be accessed by @ActivityContext
. For @InstallIn
annotation, I have passed SingletonComponent::class
because these instances are used in all most all the screens in the application so it is blind to the application lifecycle. After creating this we do not need to make any changes to the repository classes. ApiService
instance should work fine with Hilt now.
After following above stes transition from Dagger to Hilt should be completed. Clear all the files and libraries that were used previously for Dagger implantation and build the project. While this covers all the points you need in the Hilt library to implement Hilt into this project there are other features that can be helpful in certain scenarios. So Take a look at the official document to learn more.
Thanks for the post!
I was studying some of your posts on this website and I think this web site is very informative! Continue posting.