Experimenting with Dagger Hilt
Tags: android
Published: 2020-06-14 13:53:00

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 have 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 aim to 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 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:

```gradle
buildscript {
    ext.hilt_version = "2.28-alpha"
    ...
    dependencies {
        ...
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}
```

And app-level `build.gradle` file:
```gradle
...
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`

```kotlin
@Entity
data class Word(

    @PrimaryKey(autoGenerate = true)
    val id: Long,

    var word: String

)
```

Dao: `WordDao.kt`

```kotlin
@Dao
interface WordDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(word: Word)

    @Query("SELECT * FROM word")
    fun getAllWords(): LiveData<Word>

}
```

`AppDB.kt`
```kotlin
@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` 

```kotlin
@HiltAndroidApp
class HiltDemo : Application()
```

Now, we need to add an activity with a recycler view to display our word list.

`item.xml`:

```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
<?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`:

```kotlin
class WordHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val wordTV: TextView = itemView.wordTV

    fun setData(word: Word) {
        wordTV.text = word.word
    }
}
```

`WordAdapter.kt`

```kotlin
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`

```kotlin
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`: 

```kotlin
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`:

```kotlin
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`
```kotlin
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`

```kotlin
@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](https://developer.android.com/training/dependency-injection/hilt-jetpack).

Let's add the libraries in our app-level `build.gradle`.

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

```kotlin

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`:

```kotlin
@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.

![Hilt Demo Run - 1](https://i.imgur.com/REI67Fu.jpg)

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`:

```kotlin
@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.

![Hilt Demo Run - 2](https://i.imgur.com/5yiHXdV.jpg)

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.
{% github pushkar-anand/hilt-demo no-readme %}


If you are interested to learn 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