Dagger Hilt Tutorial - Step by Step Guide

Working on an Android project, we need to integrate a lot of different dependencies, and to manage these dependencies we use a dependency injection framework like Dagger.

But to setup and work with Dagger requires a large amount of boilerplate code and a very steep learning curve. It is like the raw version for Dagger without Android support. Then Dagger-Android came, which reduced the boilerplate code but was not successful.

Now, with Dagger-Hilt releasing as a part of Jetpack libraries, it is now the recommended way by Google to use it. According to Dagger-Hilt, it helps us:

  • To make the dagger code easy and simple for developers.
  • To provide a different set of bindings for different build types.
  • To just take care of where to inject dependencies and rest all of the code generations happens by dagger itself by using annotations and thus removing all the boilerplate code.

In this tutorial, we would learn:

  • Understanding Dagger
  • Setting up a new project
  • Project Structure
  • Integrating Dagger-Hilt
  • WorkManager with Dagger-Hilt
  • Qualifiers

So, let's start learning.

Understanding Dagger

Before starting with Dagger-Hilt we need to understand Dagger basics. In this section, we will help you understand the Dagger and its terminologies.

Basically, to understand Dagger we have to understand the 4 major annotations,

  • Module
  • Component
  • Provides
  • Inject

To understand it better in a basic way, think module as a provider of dependency and consider an activity or any other class as a consumer. Now to provide dependency from provider to consumer we have a bridge between them, in Dagger, Component work as that specific bridge.

Now, a module is a class and we annotate it with @Module for Dagger to understand it as Module.

A component is an interface, which is annotated with @Component and takes modules in it. (But now, this annotation is not required in Dagger-Hilt)

Provides are annotation which is used in Module class to provide dependency and,

Inject is an annotation that is used to define a dependency inside the consumer.

It is highly recommended to know about raw Dagger before moving to Dagger-Hilt.

If you are new to Dagger and want to understand these things in detail, I recommend you to watch this video.

If you already know the basics of Dagger, you can skip the video.

Setting up a new project

Here, we are going to set up the Android Project.

Our final project can be found here.

Create a Project

  • Start a new Android Studio Project
  • Select Empty Activity and Next
  • Name: Dagger-Hilt-Tutorial
  • Package name: com.mindorks.framework.mvvm
  • Language: Kotlin
  • Finish
  • Your starting project is ready now

Add dependencies

Add the following dependencies in the app's build.gradle file,

implementation "androidx.recyclerview:recyclerview:{latest-version}"
implementation 'android.arch.lifecycle:extensions:{latest-version}'
implementation 'com.github.bumptech.glide:glide:{latest-version}'
implementation 'androidx.activity:activity-ktx:{latest-version}'

Now our project is ready with dependencies.

Project Structure

For the project, we are going to follow a basic version of MVVM. Our package in the project will look like below:

We need the enum to represent the UI State. We will create that in the utils package.

package com.mindorks.framework.mvvm.utils

enum class Status {
    SUCCESS,
    ERROR,
    LOADING
}

We need a utility class that will be responsible to communicate the current state of Network Call to the UI Layer. We are naming that as Resource. So, create a Kotlin data class Resource inside the same utils package and add the following code.

package com.mindorks.framework.mvvm.utils

data class Resource<out T>(val status: Status, val data: T?, val message: String?) {

    companion object {

        fun <T> success(data: T?): Resource<T> {
            return Resource(Status.SUCCESS, data, null)
        }

        fun <T> error(msg: String, data: T?): Resource<T> {
            return Resource(Status.ERROR, data, msg)
        }

        fun <T> loading(data: T?): Resource<T> {
            return Resource(Status.LOADING, data, null)
        }

    }

}

Our utils package is ready now.

Integrating Dagger-Hilt

To setup Dagger in the project, we would add the following in the app's build.gradle file,

implementation 'com.google.dagger:hilt-android:{latest-version}'
kapt 'com.google.dagger:hilt-android-compiler:{latest-version}'

Then as a next step, we will apply the dagger.hilt plugin at the top of the app's build.gradle as well like,

apply plugin: 'dagger.hilt.android.plugin'

and finally, we will add the following in the classpath of the project's build.gradle like,

classpath "com.google.dagger:hilt-android-gradle-plugin:{latest-version}"

This is the required setup to get started to use Dagger-Hilt in the project.

Setting up Dagger-Hilt

We will break the setting up dagger hilt in the project in steps.

Step 01.

We will first update our Application class App like,

class App : Application()

and we will update the Manifest file like,

android:name=".App"

Now, to begin working with Dagger we need to annotate the application class with @HiltAndroidApp. The updated code will look like,

@HiltAndroidApp
class App : Application()

If you are planning to use Dagger-Hilt in your app, the above mention step is a mandatory one. It generates all the component classes which we have to do manually while using Dagger.

Step 02.

Now, we will add the dependencies for Retrofit and Kotlin-Coroutines in the app's build.gradle like,

// Networking
implementation "com.squareup.retrofit2:retrofit:{latest-version}"
implementation "com.squareup.retrofit2:converter-moshi:{latest-version}"
implementation "com.squareup.okhttp3:okhttp:{latest-version}"
implementation "com.squareup.okhttp3:logging-interceptor:{latest-version}"

// Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:{latest-version}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:{latest-version}"

Now, in the project what we will do is, we will do an API call and show a list of users. We will also use Kotlin-Coroutine for multithreading.

Now, we will create api, model, repository packages inside data layer. It will have files like,

Then, ApiService looks like,

interface ApiService {

    @GET("users")
    suspend fun getUsers(): Response<List<User>>

}

ApiHelper looks like,

interface ApiHelper {

    suspend fun getUsers(): Response<List<User>>
}

and finally, in ApiHelperImpl we would inject ApiService in the constructor using @Inject and implement ApiHelper.

class ApiHelperImpl @Inject constructor(private val apiService: ApiService) : ApiHelper {

    override suspend fun getUsers(): Response<List<User>> = apiService.getUsers()

}

Here, @Inject is helping in passing the dependency required by ApiHelperImpl in the constructor itself.

The User data class looks like,

data class User(
    @Json(name = "id")
    val id: Int = 0,
    @Json(name = "name")
    val name: String = "",
    @Json(name = "email")
    val email: String = "",
    @Json(name = "avatar")
    val avatar: String = ""
)

and finally, in MainRepository we will pass ApiHelper in the constructor of the repository. MainRepository looks like,

class MainRepository @Inject constructor(private val apiHelper: ApiHelper) {

    suspend fun getUsers() =  apiHelper.getUsers()

}

Now, if you can see we have passed ApiHelper and ApiService in MainRepository and ApiHelperImpl respectively. So, to inject everything in the constructor we also need to provide it using @Provide annotation in Dagger.

Step 03.

Now, we will create a package di -> module and inside it, we will create ApplicationModule. As you can see we are not creating ApplicationComponent as we will use the one provided by Dagger-Hilt itself.

We will create a class ApplicationModule and annotate it with @Module. Using this annotation will make dagger understand that this class is a module.

@Module
class ApplicationModule { }

Now, we will need to plug this module class in the specific component. In this case, we need to this at the application level so we will install it in ApplicationComponent like,

@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {}

Here, you can see that we have used @InstallIn annotation to install it in ApplicationComponent. ApplicationComponent is provided by Dagger-Hilt.

This means that the dependencies provided here will be used across the application. Let's consider that we want to use at the activity level we install the module in,

@InstallIn(ActivityComponent::class)

Similarly like ApplicationComponent/ActivityComponent, we have a different type of components like,

FragmentComponent for Fragments, ServiceComponent for Service, etc.

Step 04.

Now, inside ApplicationModule, we will provide all the dependencies one by one and the updated code of ApplicationModule class looks like,

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

    @Provides
    fun provideBaseUrl() = BuildConfig.BASE_URL

    @Provides
    @Singleton
    fun provideOkHttpClient() = if (BuildConfig.DEBUG) {
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
        OkHttpClient.Builder()
            .addInterceptor(loggingInterceptor)
            .build()
    } else OkHttpClient
        .Builder()
        .build()


    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient, BASE_URL: String): Retrofit =
        Retrofit.Builder()
            .addConverterFactory(MoshiConverterFactory.create())
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .build()

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit) = retrofit.create(ApiService::class.java)

    @Provides
    @Singleton
    fun provideApiHelper(apiHelper: ApiHelperImpl): ApiHelper = apiHelper

}

Here, we have provided dependencies using @Provide annotation, which would be accessed across the application.

@Singleton annotation helps the instance to be created and used once across the app.

Similarly, like Singleton which stays till the application lifecycle, we also have @ActivityScoped, @FragmentScoped, etc in which dependencies are scoped till the lifecycle of Activity and Fragment.

Now, if you remember in the last step, we passed ApiHelper and ApiService in MainRepository and ApiHelperImpl respectively, and to inject it successfully we need to provide these two dependencies.

In ApplicationModule, the last two functions i.e. provideApiService and provideApiHelper are providing the instance of ApiService and ApiHelper.

Also, for BASE_URL, we will add the following in the defaultConfig block in the app's build.gradle file,

buildConfigField 'String', 'BASE_URL', "\"https://5e510330f2c0d300147c034c.mockapi.io/\""

Step 05.

Now, since everything is setup, now we need to use/inject them in the Android classes. In our case, we need our activity to start using them.

So to make any Android class supported by Dagger-Hilt we use,

@AndroidEntryPoint

So, in our code, we will create another package ui, and inside it will create another sub-package called main which will have MainActivity, MainViewModel, and MainAdapter to show the list of users.

Now, we will add the AndroidEntryPoint Annotation in MainActivity like,

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {}

Here, @AndroidEntryPoint means Dagger-Hilt can now inject dependencies in this class.

@AndroidEntryPoint annotation can be used in,

  1. Activity
  2. Fragment
  3. View
  4. Service
  5. BroadcastReceiver
Hilt currently only supports activities that extend ComponentActivity and fragments that extend androidx library Fragment.

Step 06.

The MainActivity will look like,

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val mainViewModel : MainViewModel by viewModels()
    private lateinit var adapter: MainAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setupUI()
        setupObserver()
    }

    private fun setupUI() {
        recyclerView.layoutManager = LinearLayoutManager(this)
        adapter = MainAdapter(arrayListOf())
        recyclerView.addItemDecoration(
            DividerItemDecoration(
                recyclerView.context,
                (recyclerView.layoutManager as LinearLayoutManager).orientation
            )
        )
        recyclerView.adapter = adapter
    }

    private fun setupObserver() {
        mainViewModel.users.observe(this, Observer {
            when (it.status) {
                Status.SUCCESS -> {
                    progressBar.visibility = View.GONE
                    it.data?.let { users -> renderList(users) }
                    recyclerView.visibility = View.VISIBLE
                }
                Status.LOADING -> {
                    progressBar.visibility = View.VISIBLE
                    recyclerView.visibility = View.GONE
                }
                Status.ERROR -> {
                    progressBar.visibility = View.GONE
                    Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
                }
            }
        })
    }

    private fun renderList(users: List<User>) {
        adapter.addData(users)
        adapter.notifyDataSetChanged()
    }

}

and the MainAdapter class looks like,

class MainAdapter(
    private val users: ArrayList<User>
) : RecyclerView.Adapter<MainAdapter.DataViewHolder>() {

    class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(user: User) {
            itemView.textViewUserName.text = user.name
            itemView.textViewUserEmail.text = user.email
            Glide.with(itemView.imageViewAvatar.context)
                .load(user.avatar)
                .into(itemView.imageViewAvatar)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        DataViewHolder(
            LayoutInflater.from(parent.context).inflate(
                R.layout.item_layout, parent,
                false
            )
        )

    override fun getItemCount(): Int = users.size

    override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
        holder.bind(users[position])

    fun addData(list: List<User>) {
        users.addAll(list)
    }
}

Here, you can see MainViewModel being used to manage data changes.

Step 07.

Here, we want to pass the following in the constructor of ViewModel,

private val mainRepository: MainRepository
private val networkHelper: NetworkHelper

To pass this we need to first create a NetworkHelper like,

@Singleton
class NetworkHelper @Inject constructor(@ApplicationContext private val context: Context) {

    fun isNetworkConnected(): Boolean {
        var result = false
        val connectivityManager =
            context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val networkCapabilities = connectivityManager.activeNetwork ?: return false
            val activeNetwork =
                connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
            result = when {
                activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
                activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
                activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
                else -> false
            }
        } else {
            connectivityManager.run {
                connectivityManager.activeNetworkInfo?.run {
                    result = when (type) {
                        ConnectivityManager.TYPE_WIFI -> true
                        ConnectivityManager.TYPE_MOBILE -> true
                        ConnectivityManager.TYPE_ETHERNET -> true
                        else -> false
                    }

                }
            }
        }

        return result
    }
}

Here, you can see we are passing the context in the constructor of NetworkHelper. We are also annotating the context with @ApplicationContext here which means that the context we are going to use will be the context of the application.

Note: If we want to apply context of the Activity, we can use @ActivityContext which has to be provided in the module.

Step 08.

Now, as we have to pass NetworkHelper and MainRepository in MainViewModel. ViewModels are not directly supported by Dagger-Hilt and to work with Dagger-Hilt in ViewModel we use Jetpack Extensions.

First, we need to setup the dependencies in gradle for Jetpack extensions.

Let's add the following in the app's build.gradle like,

implementation 'androidx.hilt:hilt-lifecycle-viewmodel:{latest-version}'
kapt 'androidx.hilt:hilt-compiler:{latest-version}'

And to support kapt, we will add the support plugin for kapt like the following in app's build.gradle,

apply plugin: 'kotlin-kapt'

Now, to pass NetworkHelper and MainRepository we won't use ViewModelFactory here but will directly pass both of them and use the @ViewModelInject annotation like,

class MainViewModel @ViewModelInject constructor(
    private val mainRepository: MainRepository,
    private val networkHelper: NetworkHelper
) : ViewModel() {

}

Here, ViewModelInject annotation will inject the dependency using the constructor and now we will perform the operations inside MainViewModel like,

class MainViewModel @ViewModelInject constructor(
    private val mainRepository: MainRepository,
    private val networkHelper: NetworkHelper
) : ViewModel() {

    private val _users = MutableLiveData<Resource<List<User>>>()
    val users: LiveData<Resource<List<User>>>
        get() = _users

    init {
        fetchUsers()
    }

    private fun fetchUsers() {
        viewModelScope.launch {
            _users.postValue(Resource.loading(null))
            if (networkHelper.isNetworkConnected()) {
                mainRepository.getUsers().let {
                    if (it.isSuccessful) {
                        _users.postValue(Resource.success(it.body()))
                    } else _users.postValue(Resource.error(it.errorBody().toString(), null))
                }
            } else _users.postValue(Resource.error("No internet connection", null))
        }
    }
}

Here, we are fetching the users in the init block and inside the viewModelScope, we will check for internet connectivity and if the connectivity is ok then we go through the API call or else we set the value to LiveData with an error.

This user LiveData is then observed in the MainActivity to display the items in the recyclerView.

If you see in the above steps, we get the instance of ViewModel by using by viewModels()

The ViewModel which is annotated by @ViewModelInject can only be reference by Views which are annotated by @AndroidEntryPoint

As a final step, add the following permission in your Manifest file,

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

Now, we are done setting up the project and if you run the project, you would see the list of users being populated in the recyclerView.

This way we can implement the dagger-hilt in our Android Project.

You can find the final project here.

Now, let's learn about the more possibilities which can come during our Android App Development.

WorkManger with Dagger-Hilt

How we can work with Dagger-Hilt and WorkManager?

If we are using WorkManger, we use @WorkerInject to inject dependency in the constructor using the Jetpack Extensions.

We also need to add the following dependency for WorkManager,

 implementation 'androidx.hilt:hilt-work:{latest-version}'

Qualifiers

Consider an example where we have two functions returning strings values. But while providing it via Dagger, how would dagger know which class needs which string value as they both are of the same type.

To solve this issue, we use Qualifiers in Dagger.

Consider an example where we have to provide two different string one for an API key and another for some library initialization like,

@Provides
@Singleton
fun provideApiKey() = "My ApiKey"

@Provides
@Singleton
fun provideLibraryKey() = "My Library Key"

Here, the Dagger-Hilt would never build successfully as the dagger would consider both the same as they both have a string as a type and will throw an error as,

error: [Dagger/DuplicateBindings] java.lang.String is bound multiple times:

Now, to provide different types of implementation of the same return type we would need Dagger-Hilt to provide multiple bindings using qualifiers.

A qualifier is an annotation that you use to identify a specific binding for a type when that type has multiple bindings defined.

Now, to define a Qualifier we will create a file name qualifier.kt in di package and update the file as,

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ApiKey

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LibraryKey

Here, we created two different annotations ApiKey and LibraryKey and both are marked as @Qualifier.

These annotations will help us to differentiate both the implementation of ApiKey and LibraryKey.

Now, in ApplicationModule, we will update both the providers for the key by attaching the annotation we just created like,

@ApiKey
@Provides
@Singleton
fun provideApiKey():String = "My ApiKey"

@LibraryKey
@Provides
@Singleton
fun provideLibraryKey():String = "My Library Key"

Now, here you can see we have attached individual qualifiers to each String providers and now Dagger-Hilt will generate the code internally to provide these strings values.

Now, to inject them individually, we will go to MainActivity and inject the strings like,

@ApiKey
@Inject
lateinit var apiKey:String

@LibraryKey
@Inject
lateinit var libraryKey:String

And now, if we log them individually we will get,

/MainActivity: My ApiKey
/MainActivity: My Library Key

This is how you can provide multiple dependencies of the same type using qualifiers.

If you remember in NetworkHelper we used @ApplicationContext, which is also a type of Qualifier but provided by Dagger-Hilt itself.

This is how you can work with Dagger-Hilt, the new dependency injection library built on top of Dagger in your project.

Team MindOrks :)