Intermediate: Android basics · Navigation and app architecture · topic 30 of 50 · 30 min
Dependency injection with Hilt
Explanation
The problem
A ViewModel needs a repository; the repository needs an API client and a database; the API client needs an HTTP client with settings. If each class creates its own dependencies with constructors inside, everything is tightly glued together: you cannot test the ViewModel with a fake repository, and the app may accidentally create three database instances.
The idea: pass dependencies in
// Hard to test: creates its own dependency
class OrdersViewModelBad : ViewModel() {
private val repo = OrderRepository(RealApi(), RealDatabase())
}
// Easy to test: receives it (constructor injection)
class OrdersViewModel(private val repo: OrderRepository) : ViewModel()This is dependency injection: dependencies are injected (given) from outside. You can do it by hand, by creating objects in one place (an AppContainer class) and passing them down. That is perfectly fine for small apps. As apps grow, a library like Hilt writes this wiring code for you.
Hilt setup in brief
// 1. The Application class
@HiltAndroidApp
class ShopApp : Application()
// 2. Activities that need injection
@AndroidEntryPoint
class MainActivity : ComponentActivity() { /* ... */ }
// 3. ViewModels
@HiltViewModel
class OrdersViewModel @Inject constructor(
private val repo: OrderRepository,
) : ViewModel()
// 4. Classes Hilt can build itself
class OrderRepository @Inject constructor(
private val api: OrderApi,
private val dao: OrderDao,
)An @Inject constructor tells Hilt how to create a class: by calling that constructor with the dependencies it needs. In Compose, get a Hilt ViewModel with hiltViewModel() instead of viewModel(). Register the Application class in the manifest with android:name=".ShopApp".
Modules for things you cannot annotate
Some objects come from libraries (Retrofit, Room), or are interfaces. You cannot add @Inject to their constructors, so you tell Hilt how to provide them in a module: an object with @Module and @InstallIn, containing @Provides functions (or @Binds for interfaces).
@Module
@InstallIn(SingletonComponent::class) // lives as long as the app
object NetworkModule {
@Provides
@Singleton // one instance for the whole app
fun provideOrderApi(): OrderApi =
Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(/* JSON converter */)
.build()
.create(OrderApi::class.java)
}Hilt checks the whole dependency graph at compile time. If something is missing, for example a repository needs an OrderApi that no module provides, the build fails with an error naming the missing type, instead of the app crashing later on a user's phone.
When to use Hilt
- Small learning app or a few screens: manual DI with a simple container is enough.
- Medium to large app, several developers, many repositories: Hilt saves a lot of wiring code and mistakes.
- Job interviews: Hilt (and the idea of DI) is commonly asked about in Android roles.
Common mistakes
- Forgetting @HiltAndroidApp or the manifest android:name entry: the app crashes at start.
- Forgetting @AndroidEntryPoint on the activity that hosts Hilt ViewModels.
- Using viewModel() instead of hiltViewModel() for a @HiltViewModel.
- Making everything @Singleton. Only share objects that really should be app-wide.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Dependency injection with Hilt · 1/6
Dependency injection means a class receives what it needs from outside, instead of creating it.
Prefer a video by a teacher? Find free videos on “hilt dependency injection android tutorial hiltviewmodel” on YouTube ↗
In real life: Swapping a fake payment gateway in tests
A ticket-booking app's CheckoutViewModel depends on a PaymentGateway interface. In production, Hilt binds it to the real gateway client. In tests, the team replaces the binding with a FakePaymentGateway that always succeeds or always fails, so they can test every checkout path in seconds without real money or network.
interface PaymentGateway { suspend fun pay(paise: Long): Boolean }
class RealGateway @Inject constructor() : PaymentGateway {
override suspend fun pay(paise: Long) = true // would call the bank
}
@Module
@InstallIn(SingletonComponent::class)
abstract class PaymentModule {
@Binds abstract fun bindGateway(impl: RealGateway): PaymentGateway
}
@HiltViewModel
class CheckoutViewModel @Inject constructor(private val gateway: PaymentGateway) : ViewModel()Practice (0/3 done)
What is the main benefit of constructor injection?
How do you tell Hilt which class implements the interface PaymentGateway?
Take the CartViewModel from topic 2 and refactor it to receive a CartRepository through its constructor. First do manual DI: create the repository in an AppContainer and pass it via a ViewModel factory or by hand. Then (optional) add Hilt: @HiltAndroidApp, @AndroidEntryPoint, @HiltViewModel with @Inject constructor and hiltViewModel().
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Navigation ComposeNavigation and app architecture · same module
- Spring and dependency injectionJava Programming (Core to Spring Boot) · Spring Boot basics
- Dependency injection and service lifetimesC# and .NET · Web APIs with ASP.NET Core and EF Core
- Test doubles and testable designJavaScript Programming (language in depth) · Functional patterns and testing
- Test doubles: fakes, stubs and mocksC# and .NET · Testing, tooling and Unity
- Classes, objects and constructorsC# and .NET · Classes and objects
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Which annotation goes on the Application class?
Answer: @HiltAndroidApp
It triggers Hilt's code generation for the app.
What does @Inject constructor do?
Answer: Tells Hilt how to create the class
Hilt calls it with the needed dependencies.
How do you provide a Retrofit API object with Hilt?
Answer: A @Provides function in a @Module
Library objects are provided in modules.
Is Hilt required for small apps?
Answer: No, manual DI is fine for small apps
Use Hilt when the wiring grows.
Which injection style is recommended?Related
Answer: Constructor injection
Constructor injection gives final fields and easy tests.
What is dependency injection?Related
Answer: Giving a class its dependencies from outside instead of it creating them
Dependencies are supplied, usually via the constructor.
What does dependency injection mean in practice?Related
Answer: Passing dependencies in as parameters instead of using globals
It is mostly "use parameters for the things you depend on".
Which design makes test doubles easy to use?Related
Answer: Depending on interfaces passed in through the constructor
Constructor injection of interfaces.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- This is the last topic of “Navigation and app architecture”. The module exam comes next.
- Module 7 exam: Navigation and app architecture · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Saving data and calling APIs. 50 topics left in the course before the final exam.