Android Hilt — Android Developer Reference

Android Hilt has become the standard dependency injection framework for modern Android development, replacing the complexity of Dagger with a clean, lifecycle-aware, and production-ready approach. This guide serves as a complete developer reference, covering everything from Hilt fundamentals—@Inject, @Module, @Provides, scopes, components, and entry points—to advanced patterns used in real Android apps. Whether you’re building MVVM, Clean Architecture, or multi-module applications, Hilt simplifies object creation, testing, and dependency management. This article helps developers understand how Hilt actually works under the hood, why Google recommends it, and how to implement it efficiently in scalable Android projects.
1. What is Hilt & Why It Exists
Dependency Injection (DI) means: instead of a class creating its own dependencies, those dependencies are provided from outside.
Without DI:
class UserRepository {
private val api = Retrofit.Builder().build().create(UserApi::class.java)
}
With DI:
class UserRepository(private val api: UserApi)
Android apps usually have many objects — Repository, UseCase, Retrofit, Room Database, DAO, ViewModel, Worker, Navigator, SharedPreferences, Dispatchers. If every class manually creates its own dependencies, code becomes tightly coupled, testing becomes hard, object creation logic gets duplicated, and lifecycle handling becomes messy.
Hilt solves this by:
Creating dependencies in one place
Reusing shared objects safely
Connecting object lifetime to Android lifecycle
Making testing easier
Hilt answers 3 main questions:
What object do you need?
How should that object be created?
How long should that object live?
Want to Crack Android Interviews and package of 20LPA-35LPA in android. Join 12 Weeks Android Job Ready Program.
2. Core Concepts & Terminology
Dependency — An object another class needs.
class UserRepository(private val api: UserApi)
// UserApi is a dependency of UserRepository
Injection — Providing the dependency to a class.
Constructor Injection — Dependencies are given through the constructor. This is the best and most preferred way.
class UserRepository @Inject constructor(
private val api: UserApi
)
Module — A class where you tell Hilt how to provide objects it cannot construct itself (e.g. Retrofit, Room database, interface implementations, third-party classes).
Component — A container that holds and provides dependencies for a specific lifecycle.
Scope — Defines how long the same instance should live.
Why Hilt instead of plain Dagger? Dagger is powerful but verbose. Hilt simplifies Dagger for Android by generating components for Android classes, reducing boilerplate, and giving lifecycle-aware components out of the box.
Dagger = core DI engine
Hilt = Android-friendly wrapper over Dagger
Hilt works at compile time — it reads annotations and generates Dagger components behind the scenes. Errors are caught at build time, and performance is better than reflection-based DI.
3. Project Setup
In your app/build.gradle:
plugins {
id("com.google.dagger.hilt.android")
kotlin("kapt")
}
dependencies {
implementation("com.google.dagger:hilt-android:VERSION")
kapt("com.google.dagger:hilt-android-compiler:VERSION")
}
Your Application class:
@HiltAndroidApp
class MyApp : Application()
@HiltAndroidApp tells Hilt to generate the application-level dependency container. It goes only on your Application class and is the starting point for everything Hilt does.
4. Android Entry Points
For Hilt to inject dependencies into Android framework classes, mark them with @AndroidEntryPoint:
@AndroidEntryPoint
class MainActivity : AppCompatActivity()
@AndroidEntryPoint
class HomeFragment : Fragment()
Why needed? Because Android creates Activities and Fragments, not you. Hilt hooks into that creation process.
Can be used on: Activity, Fragment, View, Service, BroadcastReceiver.
5. Constructor & Field Injection
Constructor Injection (preferred):
class UserRepository @Inject constructor(
private val api: UserApi
)
Simple, testable, explicit, less boilerplate. Use this whenever possible.
Field Injection (used in Android framework classes):
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var repository: UserRepository
}
You need field injection here because you do not control the Activity constructor. Prefer constructor injection everywhere else.
6. Modules: @Provides & @Binds
Some classes cannot use constructor injection — Retrofit instances, Room database builder, interface types, third-party SDK classes. For these, use a module.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("<https://example.com/>")
.build()
}
@Provides
fun provideUserApi(retrofit: Retrofit): UserApi {
return retrofit.create(UserApi::class.java)
}
}
@Module — this class provides dependencies. @InstallIn — tells which component gets these dependencies. @Provides — tells how to create the object.
@Provides vs @Binds
Use @Provides when you manually create the object, creation needs logic, it's a third-party class, or uses a builder pattern:
@Provides
fun provideApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)
Use @Binds when binding an interface to its implementation, and the implementation already has @Inject constructor:
interface Logger
class LoggerImpl @Inject constructor() : Logger
@Module
@InstallIn(SingletonComponent::class)
abstract class LoggerModule {
@Binds
abstract fun bindLogger(impl: LoggerImpl): Logger
}
@Binds is better here — less code, more efficient, clearer intent.
7. Scopes & Lifecycles
Scope decides whether Hilt creates a new instance every time, or one shared instance for a lifecycle.
Scope | Component | Lifetime |
|---|---|---|
| SingletonComponent | Entire app |
| ActivityRetainedComponent | Survives rotation |
| ViewModelComponent | ViewModel lifetime |
| ActivityComponent | Activity instance |
| FragmentComponent | Fragment instance |
| ServiceComponent | Service lifetime |
@Singleton
class UserRepository @Inject constructor(
private val api: UserApi
)
@Singleton = one instance for the whole app, inside SingletonComponent.
Why use scopes? Save memory, preserve state where needed, avoid unnecessary object creation.
Important: Do not make everything @Singleton. Use the smallest correct lifecycle.
8. Hilt Components
Hilt creates components tied to Android lifecycles.
SingletonComponent — Lives as long as the app process. Use for Retrofit, Room database, repositories, app-wide managers.
ActivityRetainedComponent — Lives across configuration changes. Use for objects that should survive rotation.
ViewModelComponent — Lives as long as a ViewModel. Use for ViewModel-specific use cases and state holders.
ActivityComponent — Lives as long as an Activity instance.
FragmentComponent — Lives as long as a Fragment instance.
ServiceComponent — Lives as long as a Service instance.
Key distinction:
@InstallIn(SingletonComponent::class)= where the binding lives (which component holds it)@Singleton= how many instances (one shared instance)
Both are needed together for a true app-wide singleton:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Singleton
@Provides
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
Without @Singleton, Hilt may create a new instance each time even if the module is installed in SingletonComponent.
9. Qualifiers — @Named & Custom
Sometimes multiple objects have the same type. For example, you may need two OkHttpClient instances — one authenticated, one not. Hilt gets confused unless you distinguish them using qualifiers.
Custom Qualifiers (recommended for production)
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PublicClient
@AuthClient
@Provides
fun provideAuthClient(): OkHttpClient = OkHttpClient()
@PublicClient
@Provides
fun providePublicClient(): OkHttpClient = OkHttpClient()
class ApiManager @Inject constructor(
@AuthClient private val client: OkHttpClient
)
@Named (simple/quick cases)
@Named is a built-in qualifier using a string key instead of a custom annotation:
@Provides
@Named("server")
fun provideServerRetrofit(): Retrofit {
return Retrofit.Builder().baseUrl("<https://example.com/>").build()
}
@Provides
@Named("google_map")
fun provideGoogleMapRetrofit(): Retrofit {
return Retrofit.Builder().baseUrl("<https://maps.google.com/>").build()
}
class UserRepositoryImpl @Inject constructor(
@Named("server") private val retrofit: Retrofit
)
class MapRepository @Inject constructor(
@Named("google_map") private val retrofit: Retrofit
)
@Named vs Custom Qualifier
| Custom | |
|---|---|---|
Type-safe | No | Yes |
Typo-prone | Yes ( | No |
Readability | OK | Better |
Best for | Quick/simple cases | Production code |
@AuthClient retrofit: Retrofit is safer than @Named("server") retrofit: Retrofit because Hilt won't help if you mistype a string.
10. Interface Injection
Hilt cannot create an interface directly. You must provide a binding.
Wrong:
class Repo @Inject constructor(
private val service: AnalyticsService // interface — Hilt can't build this
)
Correct:
interface AnalyticsService
class FirebaseAnalyticsService @Inject constructor() : AnalyticsService
@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {
@Binds
abstract fun bindAnalyticsService(
impl: FirebaseAnalyticsService
): AnalyticsService
}
11. ViewModel Injection & SavedStateHandle
@HiltViewModel
class LoginViewModel @Inject constructor(
private val repository: AuthRepository
) : ViewModel()
@AndroidEntryPoint
class LoginFragment : Fragment() {
private val viewModel: LoginViewModel by viewModels()
}
@HiltViewModel tells Hilt to create this ViewModel and resolve its dependencies. No custom ViewModelFactory needed.
SavedStateHandle
Hilt can inject SavedStateHandle directly into a ViewModel:
@HiltViewModel
class DetailViewModel @Inject constructor(
private val repository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel()
Useful for nav arguments and restoring state after process death.
Full Architecture Chain
class UserRepository @Inject constructor(
private val api: UserApi,
private val dao: UserDao
)
class GetUsersUseCase @Inject constructor(
private val repository: UserRepository
)
@HiltViewModel
class UserViewModel @Inject constructor(
private val getUsersUseCase: GetUsersUseCase
) : ViewModel()
Hilt resolves the full chain: ViewModel → UseCase → Repository → API/DAO.
12. Room Database with Hilt
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Singleton
@Provides
fun provideDatabase(
@ApplicationContext context: Context
): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_db"
).build()
}
@Provides
fun provideUserDao(db: AppDatabase): UserDao {
return db.userDao()
}
}
@ApplicationContext — A built-in Hilt qualifier for the application Context. @ActivityContext — A built-in Hilt qualifier for activity Context.
Note: The DAO does not need @Singleton because it's a lightweight wrapper — it's the database itself that must be a singleton.
13. Retrofit / Network with Hilt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Singleton
@Provides
fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder().build()
@Singleton
@Provides
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("<https://example.com/>")
.client(client)
.build()
}
@Singleton
@Provides
fun provideApi(retrofit: Retrofit): UserApi {
return retrofit.create(UserApi::class.java)
}
}
All three are @Singleton because they are expensive to create, reused across the app, and should not be recreated often.
14. Assisted Injection
Not every value can be provided by Hilt automatically. Some values are only available at runtime — a userId, orderId, navigation argument, or any dynamic value passed from the UI.
With Assisted Injection, part of the object is provided by Hilt, and part is supplied manually at runtime.
class ReportGenerator @AssistedInject constructor(
private val apiService: ApiService, // provided by Hilt
@Assisted private val reportId: String // provided manually at runtime
)
You also need a factory:
@AssistedFactory
interface ReportGeneratorFactory {
fun create(reportId: String): ReportGenerator
}
And inject the factory instead:
class SomeViewModel @Inject constructor(
private val factory: ReportGeneratorFactory
) : ViewModel() {
fun generate(id: String) {
val generator = factory.create(id)
}
}
Simple way to think about it:
Constructor Injection = "Everything comes from Hilt"
Assisted Injection = "Some things come from Hilt, some things come from me at runtime"
Use assisted injection when: some dependencies come from Hilt, and some values are dynamic and only known while the app is running.
A very common use case is @HiltViewModel with assisted injection for passing a dynamic ID to a ViewModel.
15. Hilt with WorkManager
Workers need special integration because Context and WorkerParameters come from WorkManager, not Hilt.
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val repository: UserRepository
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// repository is injected by Hilt
return Result.success()
}
}
Also add the HiltWorkerFactory to your Application:
@HiltAndroidApp
class MyApp : Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
16. Hilt with Jetpack Compose
In your Activity:
@AndroidEntryPoint
class MainActivity : ComponentActivity()
In composables, use hiltViewModel() instead of viewModels():
@Composable
fun LoginScreen(
viewModel: LoginViewModel = hiltViewModel()
) {
// viewModel is Hilt-injected
}
Hilt still works the same way. Only the UI framework changed.
Also works with Navigation Compose:
composable("login") {
val viewModel: LoginViewModel = hiltViewModel()
LoginScreen(viewModel)
}
17. Entry Points (Advanced)
Sometimes Hilt cannot inject normally into a class — for example, a ContentProvider or a class created outside Hilt's control. Use @EntryPoint for these rare cases.
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AnalyticsEntryPoint {
fun analytics(): AnalyticsService
}
Access it via:
val entryPoint = EntryPointAccessors.fromApplication(
context,
AnalyticsEntryPoint::class.java
)
val analytics = entryPoint.analytics()
Use only when normal injection is not possible.
18. Hilt Testing
One of the biggest reasons teams use Hilt is testability — you can replace production dependencies with fakes in tests.
@HiltAndroidTest
class LoginActivityTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Before
fun setup() {
hiltRule.inject()
}
}
Replace a module in tests using @UninstallModules and a fake module:
@UninstallModules(NetworkModule::class)
@HiltAndroidTest
class UserViewModelTest {
@Module
@InstallIn(SingletonComponent::class)
object FakeNetworkModule {
@Provides
fun provideUserApi(): UserApi = FakeUserApi()
}
}
You can inject fake repositories, mock APIs, and in-memory databases. This makes UI and integration tests much cleaner.
19. Common Mistakes
1. Forgetting @AndroidEntryPoint — Injection silently fails in Activity/Fragment.
2. Using interface without @Binds or @Provides — Hilt cannot instantiate interfaces automatically.
3. Wrong scope — Making a short-lived object @Singleton unnecessarily wastes memory and can cause leaks.
4. Injecting Activity context where app context is needed — Can cause memory leaks. Use @ApplicationContext for app-level context.
5. Using field injection everywhere — Prefer constructor injection except in Android framework classes.
6. Cyclic dependencies — If A depends on B and B depends on A, Hilt cannot resolve the circular graph.
7. Installing module in wrong component — If a dependency is needed in a ViewModel but the module is installed in an incompatible component, Hilt will fail at compile time.
8. Typos with @Named — @Named("servr") and @Named("server") are different; Hilt won't warn you. Prefer custom qualifiers in production.
20. Best Practices & Summary
Best practices:
Prefer constructor injection always
Use
@Providesonly when necessary (third-party, builders)Use
@Bindsfor interface-to-implementation mappingsScope carefully — use the smallest correct lifecycle
Keep modules organized by feature or layer (NetworkModule, DatabaseModule, RepositoryModule)
Avoid one giant "everything module"
Use qualifiers for same-type dependencies
Inject abstractions (interfaces), not concrete types where useful
Make repositories and data sources test-friendly from the start
When to use Hilt: Use Hilt when your app has multiple layers, networking, a database, ViewModels, testing needs, or a medium-to-large codebase. For very tiny apps, manual DI may be enough. For real production apps, Hilt is almost always worth it.
Quick Reference — Annotations Cheat Sheet
Annotation | Purpose |
|---|---|
| Starts Hilt; goes on Application class |
| Enables injection in Activity/Fragment/etc |
| Marks constructor or field for injection |
| Marks a class as a provider of dependencies |
| Specifies which Hilt component a module belongs to |
| Tells Hilt how to create an object |
| Binds an interface to its implementation |
| One instance for the whole app |
| Enables Hilt injection into a ViewModel |
| One instance per ViewModel |
| One instance per Activity |
| One instance per Fragment |
| Creates a custom qualifier annotation |
| Built-in string-based qualifier |
| Built-in qualifier for app Context |
| Built-in qualifier for Activity Context |
| For injecting with runtime parameters |
| Marks the runtime-provided parameter |
| Factory interface for assisted injection |
| Enables Hilt injection in a Worker |
| Manual injection point for non-Hilt classes |
| Enables Hilt in instrumented tests |
| Replaces a module in tests |
In one sentence: Hilt is a compile-time, lifecycle-aware dependency injection framework for Android that automatically builds and provides object graphs using annotations.
Comments (0)
No comments yet. Be the first to share your thoughts!
Leave a Comment
Get the latest updates
Join our newsletter and never miss an article.