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