Dependency Injection with Hilt
Reviewed & published by Brayan K
Hilt is Google's opinionated dependency injection library for Android, built on Dagger. It wires your object graph at compile time so classes receive their dependencies instead of constructing them.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end you'll set up @HiltAndroidApp, use @Inject constructors, and write a @Module.
What You'll Learn in This Lesson
- Why dependency injection matters
- Enabling Hilt with @HiltAndroidApp
- Injecting with @AndroidEntryPoint
- @Inject constructors
- @Module, @Provides and @Binds
- Scopes, @InstallIn and hiltViewModel()
1️⃣ Enabling Hilt
Annotate your Application with @HiltAndroidApp to create the root container, then mark each Android class that should receive dependencies with @AndroidEntryPoint.
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.android.AndroidEntryPoint
import androidx.appcompat.app.AppCompatActivity
// 1) Enable Hilt for the whole app. This creates the root container.
@HiltAndroidApp
class MyApp : Application()
// 2) Mark Android classes that should RECEIVE injected dependencies.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// Hilt will inject these fields for you.
}These two annotations connect your Android entry points to the dependency graph Hilt generates.
2️⃣ @Inject Constructors
For classes you own, an @Inject constructor tells Hilt how to build them — and Hilt supplies their dependencies automatically. Add @Singleton for a single shared instance.
import javax.inject.Inject
import javax.inject.Singleton
// An @Inject constructor tells Hilt HOW to build this class
// and what it depends on.
class UserRepository @Inject constructor(
private val api: ApiService, // Hilt supplies this automatically
) {
suspend fun load() = api.fetchUser()
}
// @Singleton => one shared instance for the whole app.
@Singleton
class AnalyticsTracker @Inject constructor() {
fun track(event: String) { /* ... */ }
}UserRepository never builds its own ApiService — Hilt hands it over.
3️⃣ Modules with @Provides / @Binds
When you can't annotate a constructor — interfaces, library types — a @Module supplies the instructions. @Provides runs a factory method; @Binds maps an interface to an implementation. @InstallIn picks the component and scope.
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
// You cannot put @Inject on an interface or a library type,
// so a @Module supplies the instructions instead.
@Module
@InstallIn(SingletonComponent::class) // app-wide lifetime
object NetworkModule {
@Provides
@Singleton
fun provideApi(): ApiService =
Retrofit.Builder()
.baseUrl("https://api.example.com")
.build()
.create(ApiService::class.java)
}Your turn. Replace the TODOs, then compare.
// 🎯 YOUR TURN — replace each TODO.
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
// 1) Enable Hilt on the Application class.
TODO // 👉 @HiltAndroidApp
class MyApp : Application()
// 2) Let Hilt build this class by annotating the constructor.
class GreetingService TODO constructor() { // 👉 @Inject
fun greet() = "Hello from Hilt!"
}Mini-Challenge: Inject a ViewModel
Bind an interface, inject it into a @HiltViewModel, and retrieve it with hiltViewModel().
// 🎯 MINI-CHALLENGE: inject a ViewModel.
//
// 1. Create an interface Clock { fun now(): Long }.
// 2. Provide a real implementation with a @Module:
// - @Binds (or @Provides) it into SingletonComponent.
// 3. Build a @HiltViewModel class HomeViewModel
// @Inject constructor(private val clock: Clock).
// 4. In a composable, obtain it with hiltViewModel().
//
// ✅ The ViewModel receives a Clock without ever calling 'new'.
// your code hereCommon Errors (and the fix)
- "Cannot be provided without an @Inject constructor or @Provides" — Add an @Inject constructor or a module binding for that type.
- Forgetting @HiltAndroidApp — Without it on your Application, nothing else works.
- Missing @AndroidEntryPoint — An Activity/Fragment can't receive injections until it's annotated.
- Forgetting @InstallIn — Every module must declare which component its bindings live in.
Pro Tips
- 💡 Prefer constructor injection: it's the cleanest and easiest to test.
- 💡 Prefer @Binds over @Provides for simple interface-to-implementation mappings — it generates less code.
- 💡 Koin is a lighter alternative with no annotation processing if Hilt feels too heavy for a small app.
📋 Quick Reference — Hilt
| Concept | Hilt API |
|---|---|
| Enable Hilt | @HiltAndroidApp class MyApp |
| Injectable entry point | @AndroidEntryPoint |
| Buildable class | @Inject constructor(...) |
| Provide a type | @Module @Provides / @Binds |
| Choose scope | @InstallIn(...), @Singleton |
| Inject a ViewModel | hiltViewModel() |
🎉 Lesson Complete!
- ✅ DI decouples object creation from use, improving testability
- ✅ Hilt is built on Dagger with Android-friendly conventions
- ✅ @HiltAndroidApp + @AndroidEntryPoint connect the graph
- ✅ @Inject constructors and @Modules provide types
- ✅ @InstallIn and scopes control lifetime; Koin is a lighter option
Practice quiz
Which dependency injection framework is Hilt built on top of?
- Dagger
- Guice
- Spring
- Koin
Answer: Dagger. Hilt is a layer over Dagger that adds Android-specific, opinionated conventions to reduce boilerplate.
Which annotation goes on your Application subclass to enable Hilt?
- @HiltApp
- @EnableHilt
- @HiltAndroidApp
- @InjectApp
Answer: @HiltAndroidApp. @HiltAndroidApp triggers Hilt's code generation and creates the application-level dependency container.
Which annotation lets an Activity or Fragment receive injected dependencies?
- @HiltView
- @AndroidEntryPoint
- @Inject
- @Component
Answer: @AndroidEntryPoint. @AndroidEntryPoint marks an Android class (Activity, Fragment, Service) as a target for field/member injection.
When you cannot annotate a constructor (e.g. an interface or library type), what do you use?
- A second Application class
- Reflection at runtime
- Nothing — Hilt guesses
- A @Module with @Provides or @Binds
Answer: A @Module with @Provides or @Binds. A @Module supplies instructions: @Provides for a factory method, @Binds to map an interface to its implementation.
What is the difference between @Provides and @Binds?
- @Provides runs a method body to create the instance; @Binds maps an interface to an implementation with no body
- They are identical
- @Provides only works for Singletons
- @Binds is for primitives only
Answer: @Provides runs a method body to create the instance; @Binds maps an interface to an implementation with no body. @Provides executes a method to build the object; @Binds is an abstract mapping from an interface to a concrete type, which Hilt resolves more cheaply.
How do you tell Hilt how to construct a class you own?
- Add it to a list in the Application
- Annotate its constructor with @Inject
- Implement an interface
- Register it in the manifest
Answer: Annotate its constructor with @Inject. An @Inject constructor tells Hilt exactly how to build the class and what its dependencies are.
What does @InstallIn specify on a module?
- The package name
- The minimum SDK
- The Gradle module
- Which Hilt component (and thus lifecycle/scope) the bindings live in
Answer: Which Hilt component (and thus lifecycle/scope) the bindings live in. @InstallIn(SingletonComponent::class), for example, installs the bindings into a component, tying their lifetime to it.
Which scope makes a single shared instance live for the entire app?
- @ViewModelScoped
- @ActivityScoped
- @Singleton
- @FragmentScoped
Answer: @Singleton. @Singleton in the SingletonComponent yields one instance for the whole application lifetime.
How do you obtain a Hilt-injected ViewModel inside a composable?
- viewModel()
- hiltViewModel()
- MyViewModel.get()
- new MyViewModel()
Answer: hiltViewModel(). hiltViewModel() retrieves a ViewModel whose dependencies are provided by Hilt.
Which is described as a lighter-weight alternative to Hilt/Dagger?
- Koin
- Glide
- OkHttp
- Retrofit
Answer: Koin. Koin is a popular service-locator-style DI library that avoids annotation processing, trading some compile-time safety for simplicity.
Continue this course
- Previous: Local Persistence with Room
- Next: Gradle & the Kotlin Build System — build.gradle.kts, plugins, version catalogs and the Android Gradle Plugin
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What is dependency injection and why should I care?
Dependency injection (DI) is a simple idea: instead of a class creating the objects it needs, those objects are handed to it from the outside. A ViewModel that needs a repository doesn't build one with 'new'; it receives one through its constructor. This decouples object creation from object use, which buys you three big wins: testability (inject a fake repository in tests), reuse (the same class works with different implementations), and a single, clear place where your object graph is wired together rather than scattered through the codebase.
How does Hilt relate to Dagger?
Dagger is a powerful, fast, compile-time DI framework, but it has a steep learning curve and a lot of boilerplate on Android. Hilt is Google's opinionated layer on top of Dagger that bakes in the standard Android components and lifecycles for you. You annotate your Application with @HiltAndroidApp, mark Activities/Fragments with @AndroidEntryPoint, and Hilt generates the Dagger components and wiring automatically. You get Dagger's compile-time safety and performance with far less ceremony.
When do I use @Inject versus a @Module?
Use an @Inject constructor whenever you own the class and can annotate it — that's the simplest path and tells Hilt exactly how to build it. Use a @Module when you can't annotate the constructor: interfaces, classes from libraries (like a Retrofit service), or objects that need custom construction logic. Inside a module, @Provides runs a method body to create the instance, while @Binds is a lightweight abstract mapping from an interface to its implementation. Every module declares @InstallIn to say which component — and therefore which scope and lifetime — its bindings belong to.
What about scopes, hiltViewModel, and lighter alternatives?
Scopes control how long an instance lives and how widely it is shared. @Singleton in the SingletonComponent gives one instance for the whole app; @ViewModelScoped ties an instance to a ViewModel's lifetime; there are activity- and fragment-scoped variants too. To get a Hilt-managed ViewModel in Compose you call hiltViewModel(), which resolves all its constructor dependencies for you. If Hilt feels heavy for a small project, Koin is a popular lighter-weight alternative: it uses a Kotlin DSL and a service-locator approach with no annotation processing, trading a little compile-time safety for faster setup.