Dependency Injection with Hilt in Android
Basics
What is Dependency Injection?
Consider this example, you are an Android Developer who drinks coffee. To drink coffee, you need a cup (of course xD).
Implement this concept in the code
class Developer() {
fun drinkCoffee() {
// steps to drink coffee
}
}
class Cup() {
// A cup
}
From this example, you can assume the Developer class needs a Cup instance to drink coffee.
class Developer() {
fun drinkCoffee() {
val cup = Cup()
// further steps to drink coffee
}
}
class Cup() {
// A cup
}
The above approach creates a new instance of a Cup whenever the Developer drinks coffee.
Generally, do you buy a new cup whenever you drink coffee? Big No (Except for an insane rich dude)
One person needs only one cup to drink coffee.
Similarly, in programming, creating unwanted instances is costly so you need to properly handle it.
Let's come back to our example, To avoid the creation of the Cup instance every time inside the Developer class, we gonna provide a Cup instance to the Developer class. This technique is called Dependency Injection.
Cup is a dependency of the Developer class.
We can inject the dependency in two ways:
Constructor injection - Provide the dependency via the constructor
Field injection - Provide the dependency via field (setters)
class Developer(private val cup: Cup? = null) {
lateinit var fieldCup: Cup
fun drinkCoffee() {
// Use the cup here
// further steps to drink coffee
}
}
class Cup() {
// A cup
}
fun main() {
// Create cup instance
val cup = Cup()
// Constructor injection
val developer = Developer(cup)
// Field injection
val developer = Developer()
developer.fieldCup = cup
}
Dependency injection makes our code loosely coupled so easily testable and reusable.
Manual Dependency Injection
We can manually handle the dependency by creating and providing the instances as we do in the above code snippet.
It takes more time and is even hard to manage its lifecycle properly.
Example: Consider the Developer and Cup example, Having the cup all the time at your work table is unwanted, agree? You need the cup only when you drink the coffee.
Similarly, providing dependencies according to the consumer's lifecycle is crucial. For example, creating and providing an instance to an activity should happen only in the activity lifecycle. In this case, holding a singleton instance is not a good way.
Hilt
Hilt is a dependency injection library created on top of Dagger specifically for Android projects.
Hilt reduces the code boilerplates and makes dependency management easier for developers.
Setup Hilt in your project
Add dependencies
Add hilt-android-gradle-plugin
plugin to your project's root build.gradle
file.
buildscript {
ext {
...
hilt_version = '2.44.2'
}
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
Apply the plugin and add the following dependencies in your app/build.gradle
file.
...
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
...
}
dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
}
Hilt application class
@HiltAndroidApp
class App: Application()
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<application
...
android:name=".App"
... >
</application>
</manifest>
HiltAndroidApp
annotation creates a hilt application component which generates the base class for your application which acts as the application-level dependency injection container.
It obeys the Application object lifecycle.
Using Hilt in MVVM architecture
Model-View-ViewModel (MVVM) is the recommended app architecture by Jetpack/developer docs.
The simple architecture that we use to demonstrate hilt here
Activity/Fragment ---> ViewModel ---> Repository ---> Datasources
The above flow indicates the dependency of each class.
In text,
Datasources (database/network) are needed by the repository
The repository is needed by the ViewModel
The ViewModel is needed by the Activity/Fragment
Initial class setup
MainActivity.kt
package io.github.dhina17.template.activities
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import io.github.dhina17.template.R
import io.github.dhina17.template.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
}
}
MainViewModel.kt
package io.github.dhina17.template.activities
import androidx.lifecycle.ViewModel
class MainViewModel(private val repository: Repository) : ViewModel() {
}
Repository.kt
package io.github.dhina17.template.activities
import io.github.dhina17.template.Database
class Repository(database: Database) {
}
Data sources are nothing but instances (however static) of a database or network service(retrofit).
Dependency Injection using Hilt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
}
@AndroidEntryPoint - It's a predefined entry point in the hilt library that generates a hilt component for the specified Android class (list of supported classes)
Since we can't use constructor injection in an activity, We will field-inject the MainViewModel in the MainActivity.
We use @Inject annotation to inject an instance into a field of a class.
Note: The field which will be injected cannot be private.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var something: Something
}
However, in the case of ViewModel, we will take the advantage of Hilt extensions to make our work easier. We don't need to provide any binding information to the hilt. We will see about bindings later.
To provide the MainViewModel object, add @HiltViewModel annotation and @Inject annotation to the constructor of the MainViewModel class.
@Inject annotation in the constructor provides the binding information to the Hilt to generate an instance. The parameters of an annotated constructor of a class are the dependencies of a class.
Here, MainViewModel has a dependency i.e Repository
package io.github.dhina17.template.activities
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val repository: Repository
): ViewModel() {
}
Simply, in the MainActivity, get the MainViewModel object by using viewModels() delegate from KTX.
package io.github.dhina17.template.activities
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import dagger.hilt.android.AndroidEntryPoint
import io.github.dhina17.template.R
import io.github.dhina17.template.databinding.ActivityMainBinding
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
}
}
We don't need to create any Hilt Module i.e Telling hilt how to create an instance of MainViewModel. Hilt will take care of that for us.
Now, we are injecting the Repository instance into the MainViewModel.
However, hilt doesn't know how to create an instance of a Repository (i.e Other than supported android classes).
To tell hilt about that, we have to provide binding information to the hilt with a Hilt Module.
Note: We don't need to do anything if the Repository doesn't have any parameters in the constructor. @Inject annotation is only enough. Hilt will take care of that.
Create a hilt module for the Repository
package io.github.dhina17.template.activities
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
@Module
@InstallIn(ViewModelComponent::class)
object RepoModule {
@Provides
fun provideRepository(database: Database): Repository {
return Repository(database)
}
}
@ Module - Indicates that a Hilt Module
@InstallIn(ViewModelComponent::class) - To make available the dependencies in the ViewModel scope. For more info, here.
Now, hilt doesn't know how to provide a database instance so we have to tell hilt by creating a module for it again.
package io.github.dhina17.template.activities
import android.content.Context
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
fun provideDatabase(@ApplicationContext context: Context): Database {
return Database(context)
}
}
@InstallIn(SingletonComponent::class) - Hilt will create a singleton instance of the database since we require a single instance for the complete application lifecycle.
@ApplicationContext - This is a predefined Qualifier in the Hilt. Hilt will provide the application context by itself.
That's it for today. We will see some other extra use cases of Hilt in the next post.
I hope you gained some knowledge here. Share with your friends.
Thanks for reading. <3
Subscribe to my newsletter
Read articles from Dhina17 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Dhina17
Dhina17
I am a passionate and self-taught Android Developer who has good experience in AOSP, Native Android Apps (Java/Kotlin), Android for Embedded, Android in IoT, and Linux Kernel. I am more inclined to open-source, leading me to contribute to a few open-source projects. Sometimes I work in automation, scripting, and bots for myself.