DEV Community

Pushkar Anand
Pushkar Anand

Posted on

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

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

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

}
Enter fullscreen mode Exit fullscreen mode

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

)
Enter fullscreen mode Exit fullscreen mode

Dao: WordDao.kt

@Dao
interface WordDao {

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

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

}
Enter fullscreen mode Exit fullscreen mode

AppDB.kt

@Database(entities = [Word::class], version = 1, exportSchema = false)
abstract class AppDB : RoomDatabase() {
    abstract fun wordDao(): WordDao
}
Enter fullscreen mode Exit fullscreen mode

Now let's set up Hilt.
We need to add an application class annotated with @HiltAndroidApp.

HiltDemo.kt

@HiltAndroidApp
class HiltDemo : Application()
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

WordHolder.kt:

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

    fun setData(word: Word) {
        wordTV.text = word.word
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

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()

}
Enter fullscreen mode Exit fullscreen mode

MainViewModel.kt:

class MainViewModel(repository: DataRepository) : ViewModel() {
    val wordListLiveData = repository.getAllWords()
}
Enter fullscreen mode Exit fullscreen mode

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) { ... }
Enter fullscreen mode Exit fullscreen mode

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()
    }

}
Enter fullscreen mode Exit fullscreen mode

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"
    ...
}
Enter fullscreen mode Exit fullscreen mode

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() { ... }

Enter fullscreen mode Exit fullscreen mode

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)
        })

    }
}
Enter fullscreen mode Exit fullscreen mode

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

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
    }

}
Enter fullscreen mode Exit fullscreen mode

Now run the app again, make sure to uninstall the previous installation first.

Hilt Demo Run - 2

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 logo pushkar-anand / hilt-demo

Demo android application for Hilt with MVVM

If you are interested to learn mode about Hilt, check out the following resources:

Discussion (5)

Collapse
gastsail profile image
Gastón Saillén • Edited on

Great article Pushkar, I will note that in the DBModule the Room instance should be generated with @Singleton on top of @Provides since we just need one instance of the Room DB to run in the whole project since @InstallIn(ApplicationComponent::class) will not generate just one instance of it, instead it will scope the creation of that instance to the ApplicationComponent and will generate new ones each time its called in the repository.

The solution to just have 1 instance of the DB running in the lifetime of the ApplicationComponent is to annotate @Singleton above @Provides in the module

Collapse
anandpushkar088 profile image
Pushkar Anand Author

Thanks for the feedback. I will update the article with usage of @Singleton.

Collapse
karuneshpalekar profile image
Karunesh Palekar

Hey, So no need to Create ViewModel Factory ?
Hilt does it for us right?

Collapse
anandpushkar088 profile image
Pushkar Anand Author

Yes, Hilt does that under the hood. You only need to annotate the view model with @ViewModelInject

Collapse
karuneshpalekar profile image
Karunesh Palekar

Great Work !!