Phase 6 · Professional AndroidModule 29~30 min read

Dependency Injection with Hilt

Wire your app together cleanly and testably with Hilt dependency injection.

What you'll learn

As your app grows, wiring objects together by hand becomes painful and error-prone. Dependency injection automates it, and Hilt is Google's recommended DI framework for Android. It's the professional finish on the architecture you built in Phases 3 and 4 — and it makes testing dramatically easier.

By the end you'll be able to:

  • Explain what dependency injection is and why it helps
  • Set up Hilt in an app
  • Provide dependencies with @Inject, @Module, and @Provides
  • Inject repositories and ViewModels effortlessly

What dependency injection is

A dependency is any object a class needs to do its job — a ViewModel needs a repository, a repository needs a DAO and an API. Dependency injection means those objects are given to a class (usually through its constructor) rather than the class creating them itself. You've actually been doing this all along — passing a repository into a ViewModel is DI. The question is who builds and supplies them:

TheProblem.kt
// Without DI: every screen manually builds the whole dependency chain. 😫
val db = Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build()
val api = retrofit.create(NewsApi::class.java)
val repository = DefaultArticleRepository(db.articleDao(), api)
val viewModel = NewsViewModel(repository)

Doing that by hand in every screen is repetitive and fragile. A DI framework builds the whole graph for you, ensures single instances where you want them, and lets you swap implementations for tests.

Key idea

DI's real payoff is testability and flexibility. Because classes receive their dependencies, you can inject a fake repository in a test or a different implementation in a debug build — without touching the class. It's the interface-first habit from Module 24, automated.

Setting up Hilt

After adding the Gradle plugin and dependencies, two annotations bootstrap Hilt: mark your Application with @HiltAndroidApp, and each entry-point Activity with @AndroidEntryPoint:

Setup.kt
// build.gradle: apply the Hilt plugin + kapt/ksp, add hilt-android + compiler.

@HiltAndroidApp                       // 1) annotate the Application
class MyApp : Application()

@AndroidEntryPoint                    // 2) annotate the Activity
class MainActivity : ComponentActivity() { /* … */ }

@Inject & modules

Tell Hilt how to build a class by annotating its constructor with @Inject — now Hilt can create it and supply its dependencies. But Hilt can't construct everything itself: interfaces (which have no constructor) and types from libraries (Retrofit, Room). For those you write a module — @Provides functions that build them, and @Binds to map an interface to its implementation:

Modules.kt
// Constructor injection: Hilt learns how to build these.
class DefaultArticleRepository @Inject constructor(
    private val dao: ArticleDao,
    private val api: NewsApi,
) : ArticleRepository

// A module tells Hilt how to provide things it can't construct itself
// (interfaces, or types from libraries like Retrofit/Room).
@Module
@InstallIn(SingletonComponent::class)
object DataModule {

    @Provides @Singleton
    fun provideApi(): NewsApi =
        Retrofit.Builder().baseUrl(BASE_URL) /* … */ .build().create(NewsApi::class.java)

    @Provides @Singleton
    fun provideDb(@ApplicationContext ctx: Context): AppDatabase =
        Room.databaseBuilder(ctx, AppDatabase::class.java, "app.db").build()

    @Provides fun provideDao(db: AppDatabase): ArticleDao = db.articleDao()
}

// Bind the interface to its implementation:
@Module @InstallIn(SingletonComponent::class)
abstract class RepoModule {
    @Binds abstract fun bindRepo(impl: DefaultArticleRepository): ArticleRepository
}

@Singleton means "create one and reuse it" — exactly right for a database or Retrofit instance. @InstallIn(SingletonComponent::class) says these live for the whole app.

Injecting ViewModels

The magic moment: annotate a ViewModel with @HiltViewModel and @Inject its constructor, and Hilt supplies the repository automatically. In Compose, get it with hiltViewModel() — no factory, no manual construction, no passing dependencies down through screens:

NewsViewModel.kt
@HiltViewModel
class NewsViewModel @Inject constructor(
    private val repository: ArticleRepository,   // provided automatically
) : ViewModel() { /* … */ }

// In Compose — no factory, no manual wiring:
@Composable
fun NewsScreen(viewModel: NewsViewModel = hiltViewModel()) { /* … */ }

Tip

Compare this to the hand-wired mess at the top of the lesson. Hilt walks the entire graph — hiltViewModel() → ViewModel → repository → DAO + API — and builds it all. You declare what each class needs; Hilt figures out how to provide it.

Recap & quick check

Key takeaways

  • Dependency injection means giving a class its dependencies instead of it creating them.
  • Hilt automates building the dependency graph and enables swapping implementations for tests.
  • Bootstrap with @HiltAndroidApp on the Application and @AndroidEntryPoint on Activities.
  • Use @Inject constructors for your own classes; @Module + @Provides/@Binds for interfaces and library types.
  • @HiltViewModel + hiltViewModel() inject ViewModels (and their dependencies) with zero manual wiring.

Quick check

1. What is dependency injection?

2. The biggest practical benefit of DI is…

3. Which annotations bootstrap Hilt?

4. How does Hilt provide an interface like ArticleRepository?

5. How do you obtain a Hilt-injected ViewModel in Compose?

Your app is wired together cleanly. Now let's prove it works with automated tests.