Dependency Injection with Hilt in Android

Photo by Vladislav M on Unsplash

Dependency Injection with Hilt in Android

·

6 min read

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

Did you find this article valuable?

Support Dhina17 by becoming a sponsor. Any amount is appreciated!