Dependency Injection with Hilt 🗡️ - The Basics

Dependency Injection with Hilt 🗡️ - The Basics

In the previous article, we learned about what is Dependency Injection and the need for it. If you haven't read the previous article, I would recommend you to.

In the previous article, I mentioned that Libraries like Hilt and Koin helps us with Dependency Injection.

In this article, I would like to give you an essence of how we can clean up our code with Dependency Injection and also will help you with setting up and injecting dependencies using Hilt in your Android App.

What is Hilt🗡️?

You might have a question about what is hilt? According to the official documentation,

Hilt is a dependency injection library for Android that reduces the boilerplate of doing manual dependency injection in your project.

In case you wonder what is manual dependency injection, It is the one we followed in the constructor injection example in the previous article where we manually instantiated all the dependencies and pass them as a parameter to our computer

fun main() {
    var processor: Processor = Processor("Intel i5")
    var gpu: GraphicsCard = GraphicsCard("NVIDIA GeForce MX250")
    var RAM: RandomAccessMemory = RandomAccessMemory(8)
    var hdd: HardDrive = HardDrive(512)

    var myGamingPC: Computer = Computer(processor, gpu, RAM, hdd)
}

Now if you think what's the big deal? well, the problem is that this type of dependency injection requires a lot of boilerplate code and if the App which we develop is a bit complex then the amount of boilerplate code becomes hard to manage.

We also need to take care of how long the dependency objects are active so that we can clear them up if we don't really need them anymore.

And this is where a library like hilt comes in handy. Hilt provides a standard way of injecting dependencies by providing us containers containing the dependencies and also manages their lifecycle [scope].

Containers

Now you might wonder what is a container and how does it reduces boilerplate code. Let me explain with a simple example.

From our computer example, we know that our computer class depends upon Processor, GraphicsCard, RandomAccessMemory and HardDrive classes. So, for every computer object, we would like to create, we must create 4 dependency objects and hence this tends to be a lot of messy code.

Instead, we can create containers to hold our dependencies and make our code a bit clean and less messy.

class ComputerDependenciesContainer {
    val processor: Processor = Processor("Intel i5")
    val gpu: GraphicsCard = GraphicsCard("NVIDIA GeForce MX250")
    val RAM: RandomAccessMemory = RandomAccessMemory(8)
    val hdd: HardDrive = HardDrive(512)
}

And then in the main function where we need to create our computer object, we can create a ComputerDependenciesContainer object and then access the dependencies using it.

fun main() {
    val dependencyContainer: ComputerDependencyContainer = ComputerDependencyContainer()

    val myGamingPC: Computer = Computer(
        dependencyContainer.processor,
        dependencyContainer.gpu,
        dependencyContainer.RAM,
        dependencyContainer.hdd
    )
}

Now, this makes the code in our main function a lot cleaner. But still, the problem is that we need to write a lot of code and this manual way of doing is time-consuming and hence, we make use of Hilt 🗡️.

Setting up Hilt 🗡️

Now that you understand why we use hilt, it's time for us to jump in into Android Studio and get your hands dirty.

The version of hilt I've used here is the latest version available at the time of writing this article. You can always check the current version either from android docs or from dagger's official website

Open Android Studio -> Create a New Project.

Now Inside your project's root build.gradle file, add the following dependency.

NOTE: This is the project-level build.gradle file and not the app level.

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

Then inside the app-level build.gradle file [app/build.gradle], add the following dependency

plugins {
    kotlin("kapt")
    id("dagger.hilt.android.plugin")
}

android {
    ...
}

dependencies {
    implementation("com.google.dagger:hilt-android:$hilt_version")
    kapt("com.google.dagger:hilt-android-compiler:$hilt_version")
}

Since hilt uses Java 8 Features, we also must add the following into our app/build.gradle file.

android {
    ...
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

Now hit the button to sync your gradle and wait for the gradle built to finish..

One eternity later sponge bob

Alright, now once your gradle build is over, now let's get into using Hilt.

Using Hilt 🗡️

Now, first of all, every application that uses Hilt must have an Application class which is annotated by @HiltAndroidApp.

So in your Android Studio inside your main package, create a new Kotlin file with the name of MySampleApplication

NOTE: You can give whatever name for your application class. For example, if your App name is Emezon, then you can name your application class as EmezonApplication

Now inside of your application file, you must make the application class extend the Application() class and annotate it with @HiltAndroidApp.

@HiltAndroidApp
class MySampleApplication : Application() {

}

Now hilt generates an Application level component that shares the same lifecycle of your Application and hence it is available to provide the required dependencies.

Also, don't forget to add this component into your App's AndroidManifest.xml file.

<application
        android:name=".CurrencyApplication"
        ...
        ...
</application>

Injecting dependencies into Android Classes

Once we have set up all the above stuff, we can now inject dependencies into other Android Classes annotated by @AndroidEntryPoint.

Now annotate your MainActivity class like this.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

  ...
}

In order to inject the dependencies, we can use the @Inject annotation with the object which we need to inject. Now suppose that we need to inject dependencies of Computer class, then that would turn up something like

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  // Performing Field Injection using Hilt
  @Inject lateinit var processor: Processor
  @Inject lateinit var gpu: GraphicsCard
  @Inject lateinit var RAM: RandomAccessMemory
  @Inject lateinit var hdd: HardDrive
  ...
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_activity)

    // creating a computer instance with the help of field injected dependencies.
    var myGamingPC = Computer(processor, gpu, RAM, hdd)
}
}

Now what we are doing in the above snipped is called Field Injection. In the last article we saw about Constructor Injection where we injected the dependencies as a parameter to the constructor.

Another way of having dependencies for our class is by having them as fields inside our class. And the process of injecting those dependencies declared as fields in our class is known as Field Injection.

If we were about to perform Constructor Injection using Hilt, then the code would look out something like this

class Computer @Inject constructor(
  private val processor: Processor
  private val gpu: GraphicsCard,
  private val RAM: RandomAccessMemory,
  private val hdd: HardDrive
) { 
  ... 
}

In order to make hilt inject dependencies of various types, we must make hilt aware of the types of our dependencies. If the dependency is an instance of a class which we own [Classes we created], then we can indicate it by annotating our class's constructor with @Inject.

class Processor 
@Inject constructor(
  private val processorModel: String
) {
 ... 
}

// Do the same thing for other dependency classes.

But, if the dependency is an instance of some class that we do not own [like instances of classes of Libraries and Android Frameworks], then we can't annotate the constructor. In such case, we make use of Hilt Modules.

Hilt Modules

A Hilt module is a class that is annotated with @Module and @InstallIn. @Module indicates that this is a Hilt Module. In the @InstallIn annotation, we must specify a Hilt Component in order to help hilt detect which containers are available.

If you are overwhelmed by the term Hilt Component, it is used to detect the lifecycle of our dependency. We will learn in detail about Hilt Component in a separate article in the future.

For example, let's consider that we need to inject a retrofit instance into our application. We can't use @Inject since we don't have access to its constructor. Hence we are obliged to make use of the Hilt Module.

For starters, retrofit is an HTTP Client which is used to make Network requests in Android.

Now, in order to provide a retrofit instance, we will make use of a function and annotate it with @Provides. Now this will enable hilt to inject a retrofit instance on classes that depend upon it.

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    fun providePokeApi(): PokeApi{
        return Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl(BASE_URL)
            .build()
            .create(PokeApi::class.java)
    }

}

This code snippet is from my Pokedex app where I made use of Retrofit to fetch data about pokemon. PokeApi is a class where I set up the Retrofit library. I would not go into the details of Retrofit since it is beyond the scope of this article. If you are interested in my Pokedex app, have a look at it's source code from here

If you see, we have mentioned SingletonComponent::class inside the @InstallIn annotation. SingletonComponent is one of the many components provided by hilt. You will get to know about them in future articles.

There are lots of interesting things to uncover about hilt components, their lifecycle, and a bunch of other stuff. We will explore each and every point of them in great detail in upcoming articles.

I hope by now you would have understood what is the need for a library like hilt and also the basics of setting up and injecting dependencies using Hilt.

Feel free to comment for some healthy discussions. Also, subscribe to my newsletter to get notifications whenever I publish a new article.

Have Fun Coding! 👋