CategoriesLibrary

Switching to Hilt from Dagger

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 NavControllerActivityNavigator 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 NavController. 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 PlantDetailsViewModel 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.

Published by Chathuranga Shan

I am a mobile application developer who loves to get into new technologies in the Industry. Specialized in Android mobile application development with 5+ years’ experience working for different types of products across many business domains.

2 comments on “Switching to Hilt from Dagger”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.