Experimenting with Dagger Hilt
Dagger recently introduced Hilt, their next take on improving dependency injection in Android. Hilt aims to provide a standard way to incorporate Dagger dependency injection into an Android application.
In the past, I’ve tried Dagger Android and Koin and eventually settled with Koin as they provide support for ViewModels out of the box.
With the introduction of Hilt and the aim of reducing boilerplate code, I decided to try out Hilt. I would be documenting my findings through this post as I try out Hilt.
With Hilt still being in the Alpha stage, I won't migrate any existing app to Hilt but instead, build a basic MVVM app to try Hilt. We'll build an app that displays a simple list of words.
Let's fire up Android Studio and start a new project with an empty activity.
Now let's add the dependencies for Hilt, Room, LiveData, and ViewModel.
After adding all the dependencies, this is how our project-level build.gradle
looks:
buildscript {
ext.hilt_version = "2.28-alpha"
...
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
And app-level build.gradle
file:
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
def lifecycle_version = "2.2.0"
def room_version = "2.2.5"
...
// Activity Ktx for by viewModels()
implementation "androidx.activity:activity-ktx:1.1.0"
//ViewModel & LiveData
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// Room
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
//Dagger - Hilt
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
...
}
Now let's add an entity, a DAO and DB file.
Entity: Word.kt
@Entity
data class Word(
@PrimaryKey(autoGenerate = true)
val id: Long,
var word: String
)
Dao: WordDao.kt
@Dao
interface WordDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(word: Word)
@Query("SELECT * FROM word")
fun getAllWords(): LiveData<Word>
}
AppDB.kt
@Database(entities = [Word::class], version = 1, exportSchema = false)
abstract class AppDB : RoomDatabase() {
abstract fun wordDao(): WordDao
}
Now let's set up Hilt.
We need to add an application class annotated with @HiltAndroidApp
.
HiltDemo.kt
@HiltAndroidApp
class HiltDemo : Application()
Now, we need to add an activity with a recycler view to display our word list.
item.xml
:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemParent"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/wordTV"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Word"/>
</androidx.constraintlayout.widget.ConstraintLayout>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mainParent"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/wordRV"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="7"
tools:listitem="@layout/item"/>
</androidx.constraintlayout.widget.ConstraintLayout>
WordHolder.kt
:
class WordHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val wordTV: TextView = itemView.wordTV
fun setData(word: Word) {
wordTV.text = word.word
}
}
WordAdapter.kt
class WordAdapter : RecyclerView.Adapter<WordHolder>() {
private var wordList = emptyList<Word>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = WordHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
)
override fun getItemCount() = wordList.size
override fun onBindViewHolder(holder: WordHolder, position: Int) {
val word = wordList[position]
holder.setData(word)
}
fun setWordList(wordList: List<Word>) {
this.wordList = wordList
notifyDataSetChanged()
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var wordAdapter: WordAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
wordAdapter = WordAdapter()
wordRV.apply {
adapter = wordAdapter
layoutManager = LinearLayoutManager(this@MainActivity)
}
}
}
Now first let's add the Repository and ViewModel without Hilt.
DataRepository.kt
:
class DataRepository(appDB: AppDB) {
private val wordDao = appDB.wordDao()
private val executor = Executors.newSingleThreadExecutor()
fun insert(word: Word) {
executor.execute {
wordDao.insert(word)
}
}
fun getAllWords() = wordDao.getAllWords()
}
MainViewModel.kt
:
class MainViewModel(repository: DataRepository) : ViewModel() {
val wordListLiveData = repository.getAllWords()
}
Now let's use Hilt to inject all the required dependencies for us.
First, we need to inject AppDB
in Data repository. We'll use the @Inject
annotation in the Data Repository.
The @Inject
is used to annotate the constructor that Hilt should use to create instances of a class.
DataRepository.kt
class DataRepository @Inject constructor( appDB: AppDB) { ... }
At this point, Hilt doesn't know how to create an instance of AppDB
.
So let's tell Hilt how to create an instance of AppDB
. We'll use @Module
annotation to create a DB module for Hilt.
The module along with @Provides
annotation will tell Hilt how to create an instance of AppDB
.
DBModule.kt
@Module
@InstallIn(ApplicationComponent::class)
object DBModule {
@Provides
fun provideDatabase(
@ApplicationContext appContext: Context
): AppDB {
return Room.databaseBuilder(
appContext,
AppDB::class.java,
"demo.db"
).fallbackToDestructiveMigration().build()
}
}
Notice the @InstallIn
& @ApplicationContext
annotation? The @InstallIn
annotation will tell Hilt which component
each module will be used or installed in. The @ApplicationContext
annotation will tell Hilt to inject application
context.
Now, we need to inject the DataRepository in our MainViewModel. To do so we need Hilt ViewModel library first. Find the library here.
Let's add the libraries in our app-level build.gradle
.
dependencies {
...
def hilt_jetpack = "1.0.0-alpha01"
...
//Dagger - Hilt
...
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hilt_jetpack"
kapt "androidx.hilt:hilt-compiler:$hilt_jetpack"
...
}
At this point, Hilt knows how to create an instance of data repository. So we'll just use the @ViewModelInject
annotation.
The @ViewModelInject
annotations will tell Hilt to inject the DataRepository
in the ViewModel.
class MainViewModel @ViewModelInject constructor(repository: DataRepository) : ViewModel() { ... }
Now all these hard work annotating our classes will only pay off when we inject the ViewModel into our MainActivity
.
To inject anything into activities, fragments, views, services and broadcast receivers we need to annotate the class
with @AndroidEntryPoint, and then annotate all the variable we want to inject with the @Inject annotation. The variables
should be a lateinit var
or should be delegated.
The variables would be available to use after the super calls in the overridden functions. For example super.onCreate()
call in an Activity.
One important thing to note here is that the activities you want to inject in must be a ComponentActivity. Since the AppCompatActivity class extends from this class, this isn't an issue.
So now, let's inject our view model in MainActivity. Wait, we don't need to, The Hilt Jetpack docs states that
An activity or a fragment that is annotated with @AndroidEntryPoint can get the ViewModel instance as normal >using ViewModelProvider or the by viewModels() KTX extensions.
So let's just annotate our activity and initiate the view model.
MainActivity.kt
:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
...
private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
mainViewModel.wordListLiveData.observe(this, Observer {
wordAdapter.setWordList(it)
})
}
}
Now let's run and see our app. If your app crashed, it's probably because you forgot to add the applications class in the Manifest like me.
Umm.. the activity is empty. We should have pre-populated the database to test the app. No problem let's do it now. We'll need to modify our DBModule to do so.
DBModule.kt
:
@Module
@InstallIn(ApplicationComponent::class)
object DBModule {
lateinit var appDB: AppDB
private val executor = Executors.newSingleThreadExecutor()
@Provides
fun provideDatabase(
@ApplicationContext appContext: Context
): AppDB {
appDB = Room.databaseBuilder(
appContext,
AppDB::class.java,
"demo.db"
).fallbackToDestructiveMigration()
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
val dao = appDB.wordDao()
executor.execute {
var word = Word(0, "Hey")
dao.insert(word)
word = Word(0, "the")
dao.insert(word)
word = Word(0, "app")
dao.insert(word)
word = Word(0, "worked")
dao.insert(word)
}
}
})
.build()
return appDB
}
}
Now run the app again, make sure to uninstall the previous installation first.
Ahh!! it works. So we built a basic MVVM with Hilt. We were able to build this app with very little boilerplate requirements as compared to Dagger Android. A lot of the work is handled for us by Hilt.
Checkout the project built while working on this post: https://github.com/pushkar-anand/hilt-demo
If you’re interested in learning mode about Hilt, check out the following resources:
- https://dagger.dev/hilt/
- https://developer.android.com/training/dependency-injection/hilt-android
- https://developer.android.com/training/dependency-injection/hilt-jetpack
- https://medium.com/androiddevelopers/dependency-injection-on-android-with-hilt-67b6031e62d
- https://joebirch.co/android/exploring-dagger-hilt-an-introduction/
- https://codelabs.developers.google.com/codelabs/android-hilt