Getting Started with Dagger Hilt in an MVVM App (Android Dependency Injection in 2021)

When it came to dependency injection in Android, it’s no question that Dagger has always taken the lead as the definitive library to use for it.

Though setting up Dagger in an app has always been an arduous task, the benefits it presented afterwards are incomparable. An app without dependency injection after all, does very quickly become hard to manage and scale as classes depend on each other a lot more.

But the problem remained. Dagger is very complex and convoluted to set up. Does it really need to be this way? Well, apparently not.

That’s why Hilt was made. Hilt streamlines the whole process, getting rid of all the unnecessary steps without compromising any of the power that the original Dagger versions offered.

The official Android Developer docs have their own tutorial on Hilt, but I’m gonna go through it down below in an app using the MVVM architecture.

 

Import the Dependencies

In your project-level build.gradle file, add the Hilt Android Gradle Plugin classpath under dependencies.

dependencies {
    classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1'
}

In your app-level build.gradle file, add the kapt (if you don’t already have it) and the Hilt plugins at the top of the file.

plugins {
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

Then in the same file, under dependencies, add the Hilt dependencies and compilers, as well as the activity-ktx and Glide dependencies which we’ll need later.

implementation 'androidx.activity:activity-ktx:1.3.1'
implementation 'com.github.bumptech.glide:glide:4.9.0'

implementation 'com.google.dagger:hilt-android:2.38.1'
kapt 'com.google.dagger:hilt-android-compiler:2.38.1'
kapt 'androidx.hilt:hilt-compiler:1.0.0'

Also be aware that the versions above are only up-to-date as of the time of this writing.

Create the Application Class

Create a new application class, and annotate it with @HiltAndroidApp. This annotation marks this as the class Dagger components should be generated.

@HiltAndroidApp
class HiltApplication : Application()

Don’t forget to set this application class in your manifest.

<application
    android:name=".HiltApplication"
    ...
>

 

Create a Repository

black framed eyeglasses

We’re not gonna go into using any api services, retrofit, or any of that here to keep the scope of this tutorial small, but we will create a repository with some sample data which we will inject into our view model later on.

We’ll use a list of dog breeds for this tutorial. Start by creating the Dog data class.

data class Dog(
    val image: String,
    val breed: String,
)

For the repository itself, we’ll abstract it with interface and implementation classes. Create the DogsRepository interface.

interface DogsRepository {
    fun getBreeds(): List<Dog>
}

And then its implementation. Disclaimer: I pulled these image urls from google images so they are definitely not mine.

class DogsRepositoryImpl : DogsRepository {

    override fun getBreeds() = listOf(
        Dog("https://bit.ly/3le3v5K", "German Shepherd"),
        Dog("https://bit.ly/3nmHmVv", "Labrador Retriever"),
        Dog("https://bit.ly/2X7o9vQ", "Pomeranian"),
        Dog("https://bit.ly/3k4h9Zz", "Siberian Husky"),
        Dog("https://bit.ly/392MUfw", "Shiba Inu"),
        Dog("https://bit.ly/3num6Nt", "Golden Retriever"),
        Dog("https://puri.na/3leLofL", "Bulldog"),
        Dog("https://bit.ly/3k50Trn", "Poodle"),
        Dog("https://bit.ly/3hmHgcI", "American Pit Bull Terrier"),
        Dog("https://bit.ly/3C1dGkI", "Chihuahua"),
        Dog("https://bit.ly/2X9LX2J", "Dobermann"),
    )
}

I also shortened the image urls otherwise it just looks really messy on here.

 

Create the Module

Under a new package di/module, create a new class called AppModule, and annotate it with @Module and @InstallIn(SingletonComponent::class).

@Module
@InstallIn(SingletonComponent::class)
class AppModule {
    // Add the provision here later
}

@Module as you might guess, marks this class as a module, aka a class that provides dependencies to other classes within its component’s scope.

@InstallIn defines the component the module is allowed to provide dependencies to. SingletonComponent is a top-level component that allows the module to inject dependencies across the entire application.

Component Injector for
SingletonComponent Application
ViewModelComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent View with @WithFragmentBindings
ServiceComponent Service

Each component and their injection scopes (from the Hilt docs)

Above are the different components and the scopes they cover.

As a best practice, it’s good to have different modules which cover the smallest scope they need to provide dependencies to. This allows you to keep your app more separated and manageable as more classes and dependencies get created and needed.

Now in AppModule, add the provision for DogsRepository using the @Provides annotation.

@Provides
@Singleton
fun provideDogsRepository(): DogsRepository = DogsRepositoryImpl()

We’re using the @Singleton annotation so we only ever inject the same one instance of DogsRepository anywhere it’s requested. Use this only when needed.

Create the Activity & ViewModel

person using smartphone

Create a new package ui/main, move MainActivity there, and create a new class MainViewModel.

@HiltViewModel
class MainViewModel @Inject constructor(
    private val dogsRepository: DogsRepository
) : ViewModel() {

    private val dogBreedsEmitter = MutableLiveData<List<Dog>>()
    val dogBreeds: LiveData<List<Dog>> = dogBreedsEmitter

    init {
        loadDogBreeds()
    }

    private fun loadDogBreeds() {
        dogBreedsEmitter.value = dogsRepository.getBreeds()
    }
}

Use the @HiltViewModel annotation here, which allows the view model to be created using Hilt’s view model factory which in turn makes it easier to be used in activities, fragments, etc.

Exactly one constructor in the view model must be annotated with @Inject. In this constructor, this is where you add all the dependencies you need injected in your view model.

Do note that you can use @Inject constructor in any class where you need dependency injection, like our repository class if we needed it, as long as you have the dependency provided in an appropriate module.

The rest of the class is just a very simple implementation of getting a list of dog breeds from the repository and passing it into the LiveData.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels()

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

        observeDogBreeds()
    }

    private fun observeDogBreeds() {
        viewModel.dogBreeds.observe(this) {
            // TODO: add an adapter
        }
    }
}

Finally, we have our activity. It’s important that we annotate with @AndroidEntryPoint which marks the Android component class to be setup for injection. This annotation can be used on most Android components including activities, fragments, views, services, and broadcast receivers.

We get our view model using the by viewModels() delegate. Then the rest is just observing our live data.

That’s our entire Hilt setup accomplished.

Finish off the app

Our Hilt setup is accomplished and we now have dependency injection running in our app, but our app still runs as a blank slate. Let’s fix that. Feel free to copy-paste the rest of this.

main_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.main.MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/dogs_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

list_item_dog.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:scaleType="centerCrop"
        android:importantForAccessibility="no"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        tools:background="@color/black" />

    <TextView
        android:id="@+id/breed"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:textColor="@color/black"
        android:textStyle="bold"
        android:textSize="20sp"
        app:layout_constraintStart_toEndOf="@id/image"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        tools:text="Dog Breed" />

</androidx.constraintlayout.widget.ConstraintLayout>

DogsAdapter.kt

class DogsAdapter(private val dogs: List<Dog>) : RecyclerView.Adapter<DogsAdapter.ViewHolder>()  {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item_dog, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(dogs[position])
    }

    override fun getItemCount() = dogs.size

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        fun bind(dog: Dog) {
            Glide.with(itemView.context)
                .load(dog.image)
                .into(itemView.findViewById(R.id.image))
            itemView.findViewById<TextView>(R.id.breed).text = dog.breed
        }
    }
}

MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels()

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

        observeDogBreeds()
    }

    private fun observeDogBreeds() {
        viewModel.dogBreeds.observe(this) {
            val dogsList = findViewById<RecyclerView>(R.id.dogs_list)
            dogsList.layoutManager = LinearLayoutManager(this)
            dogsList.adapter = DogsAdapter(it)
        }
    }
}

Now you should have a finished result that looks like this

Get the Source Code

You can find this project’s source code on Github.

I hope you found this tutorial useful. If you did like it and want to see more, follow me on Twitter and Instagram where I post almost daily. It also means the world to me <3 and as always, happy coding ༼ つ ◕_◕ ༽つ

Extra Thoughts

“Does this work with Retrofit, Coroutines, RxJava, etc.?”

Yes, Hilt works seamlessly with all of these. I didn’t include it in this tutorial to keep the scope small and basic, but do let me know if you want a more comprehensive tutorial involving these.

“findViewById? Eww”

Yeah I know, but again, scope. I don’t want to have to talk about view binding, data binding, or kotlin-extensions in a tutorial about Hilt.

Troubleshooting

A common error to find is this one.

Expected @HiltAndroidApp to have a value. Did you forget to apply the Gradle Plugin?

If you encounter this, check your Kotlin Gradle Plugin version in your project-level build.gradle file. Hilt doesn’t seem to work with version 1.5.20. Upgrading the version should fix the issue.

Subscribe to the Newsletter