Dependency Injection with Hilt
๐ Dependency Injection with Hilt
๐ค The Problem, Concretely
Chapter 4's UsersViewModel needed an ApiService โ but where does that instance actually come from? Without DI, every class needing it either constructs its own (duplicating the Retrofit setup everywhere) or reaches into some global singleton object directly:
This works, but it hardcodes exactly which ApiService implementation UsersViewModel uses โ there's no way to substitute a fake one for testing, and every class that needs an ApiService repeats this same construction line. Dependency injection flips the direction: a class declares what it needs as a constructor parameter, and something else โ here, Hilt โ is responsible for actually providing a real instance.
UsersViewModel no longer knows or cares how ApiService is built โ a real one in production, a fake one in tests, both work identically as far as this class is concerned.
๐ Hilt Setup โ Two Annotations to Start
@HiltAndroidApp generates Hilt's base dependency container for the whole app โ it must go on a custom Application subclass, registered in AndroidManifest.xml's <application android:name=".MyApplication">. @AndroidEntryPoint marks an Activity (or Fragment) as able to receive Hilt-provided dependencies โ without it, Hilt won't wire anything into that class at all.
๐ฆ @Module + @Provides โ For Things You Don't Own
Hilt can't automatically construct classes it doesn't control the source of โ Retrofit and RoomDatabase come from external libraries, so a module tells Hilt explicitly how to build them:
@InstallIn(SingletonComponent::class) scopes these provisions to the whole app's lifetime; @Singleton on each function ensures only one instance is ever created and reused โ this is precisely the manual companion object singleton pattern from Chapter 3, now handled declaratively. Notice provideApiService takes a retrofit: Retrofit parameter โ Hilt automatically supplies it from provideRetrofit above, chaining dependencies together without any manual ordering.
๐ท๏ธ @Inject Constructor โ For Things You Do Own
For a class you wrote yourself (like a repository wrapping the DAO and API calls together), no module is needed at all โ just annotate the constructor:
Hilt sees @Inject constructor, recognizes it can build a UserRepository by supplying an ApiService (from NetworkModule) and a TaskDao (assuming a similar DatabaseModule providing it), and does so automatically wherever a UserRepository is itself requested โ no explicit "here's how to build a UserRepository" module is required, since Hilt can already see the whole recipe directly in the constructor.
๐ Injecting Into a ViewModel
@HiltViewModel plus hiltViewModel() (instead of Chapter 1's plain viewModel()) is the only change needed at the call site โ everything about how the ViewModel survives rotation, scopes to the screen, and exposes StateFlow works identically to Chapter 2, just with its dependencies supplied automatically instead of hardcoded inside the class.
Manual Wiring vs Hilt โ What Actually Changed
| Manual (Chapters 3-4) | Hilt | |
|---|---|---|
| Database singleton | Hand-written companion object + synchronized block | @Provides @Singleton function |
| ViewModel construction | Passed a dao/api manually via a ViewModelFactory | @Inject constructor โ Hilt supplies it |
| Swapping a real dependency for a test fake | Requires editing the class itself | Swap the @Module's binding โ the class is unchanged |
๐ป Coding Challenges
Challenge 1: Application and Activity Setup
Add a custom Application class annotated @HiltAndroidApp, register it in AndroidManifest.xml, and annotate MainActivity with @AndroidEntryPoint. Add a comment explaining what would go wrong if @AndroidEntryPoint were forgotten on an Activity that needs a Hilt-injected ViewModel.
Goal: Practice the minimum Hilt setup every project needs.
Challenge 2: A Module for Room
Write a DatabaseModule (@Module, @InstallIn(SingletonComponent::class)) with @Provides @Singleton functions providing an AppDatabase (using Room.databaseBuilder with an applicationContext parameter) and a TaskDao (from that database's taskDao() function).
Goal: Practice writing a @Module for a dependency Hilt can't construct automatically, following the chapter's NetworkModule pattern for a different kind of dependency.
Challenge 3: A Repository and Injected ViewModel
Write a TaskRepository with an @Inject constructor taking a TaskDao, exposing a getAllTasks(): Flow<List<Task>> function that delegates to the DAO. Write a @HiltViewModel TaskViewModel with an @Inject constructor taking the repository, exposing its tasks as a StateFlow via stateIn (Chapter 3's pattern).
Goal: Practice the full chain: DAO โ Repository โ ViewModel, all wired by Hilt with no manual construction anywhere.
DI's benefit doesn't show up much in a single-screen toy app โ the manual approach from Chapters 3-4 works fine at that scale. It pays off as an app grows (many classes sharing the same dependencies, without each repeating construction logic) and especially once automated testing enters the picture, where swapping a real network/database dependency for a fake one is exactly what DI is designed to make painless.
๐ฏ What's Next
Next chapter: Coroutines in Android โ viewModelScope, lifecycleScope, and choosing between the IO and Main dispatchers in an Android-specific context.