Skip to main content
Android Development with Kotlin
CHAPTER 28 Beginner

Dependency Injection (Hilt/Dagger)

Updated: May 16, 2026
30 min read

# CHAPTER 28

Dependency Injection (Hilt/Dagger)

1. Introduction

Throughout this curriculum, we have built complex, layered architectures. The Activity needs a ViewModel. The ViewModel needs a Repository. The Repository needs a Room DAO and a Retrofit API. Currently, we are manually instantiating these classes inside each other: val repository = UserRepository(Database.get(context), Retrofit.api). As an app scales to hundreds of screens, this manual "wiring" becomes a chaotic, unmaintainable nightmare of boilerplate code. Furthermore, it makes Unit Testing nearly impossible. The industry solution is Dependency Injection (DI). In this chapter, we will master Hilt, Google's official, Jetpack-recommended DI framework built on top of the powerful Dagger library. Hilt automatically constructs and provides dependencies exactly where they are needed.

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Understand the core philosophy of "Inversion of Control" and Dependency Injection.
  • Configure Hilt Gradle dependencies and plugins.
  • Initialize Hilt at the Application level via @HiltAndroidApp.
  • Annotate UI controllers (Activities/Fragments) with @AndroidEntryPoint.
  • Construct Hilt Modules and leverage @Provides to instruct Hilt on object creation.
  • Seamlessly inject dependencies into ViewModels utilizing @HiltViewModel.

3. What is Dependency Injection?

Without DI: "I am the ViewModel. I will build my own Repository utilizing the new keyword." (High Coupling). With DI: "I am the ViewModel. I need a Repository. Hey Hilt, please build one and hand it to me in my constructor." (Low Coupling, High Testability).

*Analogy:* You are a Chef. You shouldn't have to build your own oven before cooking. The restaurant (Hilt) should "inject" an oven into your kitchen.

4. Step 1: Hilt Gradle Setup

Hilt requires several plugins. Project-level build.gradle.kts:
kotlin
123
plugins {
    id("com.google.dagger.hilt.android") version "2.50" apply false
}

App-level build.gradle.kts (Module :app):

kotlin
123456789
plugins {
    id("kotlin-kapt") // Required for Hilt code generation!
    id("com.google.dagger.hilt.android")
}

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

5. Step 2: The Application Class

Hilt needs a starting point to attach its massive, generated dependency graph.
  1. 1. Create a new Kotlin class named MyApplication extending Application.
  1. 2. Annotate it with @HiltAndroidApp.
  1. 3. CRITICAL: Register this class in the AndroidManifest.xml inside the <application> tag using android:name=".MyApplication".
kotlin
12345678910
import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp // Triggers Hilt's code generation, including a base class for your app
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // App initialization code
    }
}

6. Step 3: Hilt Modules (@Provides)

Hilt is smart, but it doesn't know how to build a Retrofit instance or a Room Database unless you teach it. We do this inside a Module.
kotlin
12345678910111213141516171819202122232425262728293031323334
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
// InstallIn(SingletonComponent) tells Hilt: "These objects should live forever as Singletons"
@InstallIn(SingletonComponent::class) 
object AppModule {

    // Teach Hilt how to build the Room Database
    @Provides
    @Singleton // Ensure only ONE instance is ever created
    fun provideUserDatabase(@ApplicationContext context: Context): UserDatabase {
        return Room.databaseBuilder(
            context,
            UserDatabase::class.java,
            "user_database"
        ).build()
    }

    // Teach Hilt how to pull the DAO out of the Database
    @Provides
    @Singleton
    fun provideUserDao(database: UserDatabase): UserDao {
        return database.userDao()
    }
    
    // (You would add another @Provides function here to build Retrofit!)
}

7. Step 4: Injecting the Repository

Now that Hilt knows how to build a UserDao, we can tell Hilt to automatically build the Repository. We do this by adding @Inject constructor.
kotlin
123456789
import javax.inject.Inject

// Hilt looks at the constructor, sees it needs a UserDao, finds the @Provides function we just wrote,
// builds the Dao, and automatically injects it right here! Magic!
class UserRepository @Inject constructor(
    private val userDao: UserDao
) {
    fun getAllUsers() = userDao.getAllUsers()
}

8. Step 5: Injecting the ViewModel

Injecting ViewModels used to be very difficult. Jetpack Hilt makes it trivial via the @HiltViewModel annotation.
kotlin
123456789101112
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
// Hilt automatically injects the UserRepository into the ViewModel!
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {

    val users = repository.getAllUsers()
}

9. Step 6: Injecting the Activity (@AndroidEntryPoint)

Finally, we must tell Hilt that our Activity wants to use injected dependencies. We annotate the Activity with @AndroidEntryPoint and use the viewModels() delegate.
kotlin
123456789101112131415161718192021
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint // Tells Hilt to inject dependencies into this Activity
class MainActivity : AppCompatActivity() {

    // Hilt handles the entire instantiation process! No ViewModelProvider factories needed!
    private val viewModel: UserViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // The ViewModel is fully wired with the Repository, Dao, and Database automatically!
        viewModel.users.observe(this) { userList ->
            println("We have ${userList.size} users!")
        }
    }
}

10. Common Mistakes

  • Forgetting the Manifest: You can write perfect Hilt code, but if you forget to add android:name=".MyApplication" to the AndroidManifest.xml, Hilt will simply never start, and your app will crash instantly with injection errors.
  • Missing @AndroidEntryPoint on Fragments: If an Activity uses Hilt, but a Fragment inside it requests a ViewModel without having @AndroidEntryPoint above its class declaration, the app will crash.

11. Best Practices

  • Scoping Constraints: Do not arbitrarily add @Singleton to every @Provides function. A Database should be a Singleton. But a simple temporary helper class should not be a Singleton, as it will persist in RAM forever and cause memory bloat. Let Hilt create new instances of lightweight objects.

12. Exercises

  1. 1. Setup Hilt dependencies in Gradle and create the MyApplication class. Don't forget the Manifest.
  1. 2. Create an AppModule and write a @Provides function that returns a String representing a base API URL. Inject that string into a dummy Repository.

13. Coding Challenges

Challenge: Refactor a previous project. Take the app you built in Chapter 22 (Repository Pattern). Completely remove all manual instantiation of the Database, Retrofit, and Repository. Implement Hilt Modules and @Inject constructors to fully automate the dependency graph.

14. MCQ Quiz with Answers

Question 1

In the context of software architecture, what structural problem does Dependency Injection (DI) primarily solve?

Question 2

Within the Dagger Hilt framework, what is the explicit functional purpose of the @Provides annotation inside an @Module class?

15. Interview Questions

  • Q: Explain the mechanics of "Inversion of Control". How does passing an Interface into a constructor via DI, rather than instantiating a concrete class directly, radically improve the testability of a system?
  • Q: Detail the lifecycle hierarchies utilized by Hilt Components. Why must a Room Database be installed within the SingletonComponent rather than the ActivityComponent?
  • Q: Contrast traditional Dagger 2 with modern Dagger Hilt. What specific Android-centric boilerplate configuration steps did Hilt eliminate to become the Jetpack standard?

16. Summary

In Chapter 28, we conquered one of the most complex architectural paradigms in software engineering: Dependency Injection. By integrating Dagger Hilt, we eliminated massive amounts of rigid boilerplate code. We established an automated dependency graph utilizing @Module and @Provides configurations. We successfully utilized @HiltViewModel and @AndroidEntryPoint to automatically wire our entire MVVM architecture from the Database to the UI. Our application is now modular, highly decoupled, strictly testable, and engineered to enterprise-level scalable standards.

17. Next Chapter Recommendation

The application is flawlessly architected, comprehensively tested, and entirely complete. The final step of any developer's journey is delivering the product to the world. Proceed to Chapter 29: Publishing to the Google Play Store to master Application Bundles, cryptographic signing, and the developer console.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·