Dependency Injection Using Hilt
1. What is Dependency Injection?
Dependency Injection means giving required objects to a class from outside instead of creating them inside the class.
Example without DI:
class UserRepository {
private val api = UserApi()
}Problem:
UserRepository creates UserApi itself.
Hard to test.
Hard to replace UserApi with FakeUserApi.
Class becomes tightly coupled.With DI:
class UserRepository(
private val api: UserApi
)Now UserRepository does not create UserApi. Someone else provides it.
Simple meaning:
Without DI:
Class creates its own dependencies.
With DI:
Class receives dependencies from outside.---
2. Real-life analogy
Suppose you are a mechanic.
Bad design:
Mechanic creates screwdriver, wrench, engine, battery by himself.Good design:
Garage provides tools to mechanic.
Mechanic only does repair work.In Android:
ViewModel needs Repository.
Repository needs API and DAO.
API needs Retrofit.
DAO needs Room Database.
Hilt provides all these objects automatically.---
3. What is Hilt?
Hilt is a dependency injection library for Android built on top of Dagger.
It reduces boilerplate code and provides standard Android containers for DI. Android documentation describes Hilt as a library that streamlines dependency management in Android apps and reduces manual dependency-injection boilerplate. ([Android Developers][1])
Simple meaning:
Hilt automatically creates and provides objects for your Android classes.Example:
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel()Here Hilt gives UserRepository to UserViewModel.
---
4. Why use Hilt?
Without Hilt:
val api = Retrofit.Builder().build().create(UserApi::class.java)
val repository = UserRepository(api)
val viewModel = UserViewModel(repository)With Hilt:
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel()Benefits:
Less boilerplate
Easy testing
Easy object sharing
Cleaner architecture
Works well with ViewModel, Repository, Retrofit, Room, WorkManager---
5. Hilt basic flow
Application starts
|
v
Hilt creates dependency container
|
v
Activity/Fragment asks for ViewModel
|
v
Hilt creates ViewModel
|
v
Hilt injects Repository
|
v
Hilt injects API/DAO into RepositoryDiagram:
MainActivity
|
v
UserViewModel
|
v
UserRepository
|
+--> UserApi
|
+--> UserDao---
6. Important Hilt annotations
| Annotation | Meaning |
|---|---|
@HiltAndroidApp | Starts Hilt in Application class |
@AndroidEntryPoint | Allows injection in Activity/Fragment/Service |
@Inject | Tells Hilt how to create object or inject field |
@HiltViewModel | Allows Hilt to create ViewModel |
@Module | Class/object that provides dependencies |
@InstallIn | Defines where dependency lives |
@Provides | Manually create and provide object |
@Binds | Bind interface to implementation |
@Singleton | One object for whole app |
@ApplicationContext | Provides application context |
@ActivityContext | Provides activity context |
---
7. Step-by-step Hilt setup
Step 1: Add plugins
Project/module setup may vary by Gradle version, but normally you add the Hilt plugin and dependency.
plugins {
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}Dependencies:
dependencies {
implementation("com.google.dagger:hilt-android:2.51.1")
kapt("com.google.dagger:hilt-android-compiler:2.51.1")
}For Kotlin Symbol Processing:
plugins {
id("com.google.devtools.ksp")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.51.1")
ksp("com.google.dagger:hilt-android-compiler:2.51.1")
}Use the latest stable version compatible with your project.
---
Step 2: Create Application class
@HiltAndroidApp
class MyApp : Application()Manifest:
<application
android:name=".MyApp">
</application>Important:
@HiltAndroidApp triggers Hilt code generation.
It creates the app-level dependency container.The official Hilt docs state that apps using Hilt must include an Application class annotated with @HiltAndroidApp. ([Android Developers][1])
---
Step 3: Add @AndroidEntryPoint
Activity:
@AndroidEntryPoint
class MainActivity : AppCompatActivity()Fragment:
@AndroidEntryPoint
class UserFragment : Fragment()Meaning:
This Android class can receive dependencies from Hilt.The Android docs explain that Hilt can provide dependencies to Android classes annotated with @AndroidEntryPoint. ([Android Developers][1])
---
8. Constructor injection
This is the cleanest way.
class UserRepository @Inject constructor(
private val api: UserApi
) {
suspend fun getUser(): User {
return api.getUser()
}
}Meaning:
Hilt knows how to create UserRepository because constructor has @Inject.If `UserRepository` needs `UserApi`, Hilt will also try to provide `UserApi`.---
9. ViewModel injection
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
fun loadUser() {
viewModelScope.launch {
val user = repository.getUser()
}
}
}Activity:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
}Fragment:
@AndroidEntryPoint
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
}Meaning:
You do not manually create ViewModel.
Hilt creates it and injects Repository.---
10. Why do we need Module?
Constructor injection works when you own the class.Example:
class UserRepository @Inject constructor(...)But you cannot add @Inject constructor to classes like:
Retrofit
OkHttpClient
Room Database
SharedPreferences
Third-party SDK classes
InterfacesFor these, use `@Module`.---
11. @Provides example with Retrofit
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideUserApi(retrofit: Retrofit): UserApi {
return retrofit.create(UserApi::class.java)
}
}Meaning:
Hilt does not know how to create Retrofit.
So we teach Hilt using @Provides.@Module tells Hilt how to provide dependencies, and @InstallIn tells Hilt which generated component the module is installed into. ([Android Developers][1])
---
12. @Binds example with Repository interface
Domain layer:
interface UserRepository {
suspend fun getUser(): User
}Data layer:
class UserRepositoryImpl @Inject constructor(
private val api: UserApi
) : UserRepository {
override suspend fun getUser(): User {
return api.getUser()
}
}Hilt module:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(
impl: UserRepositoryImpl
): UserRepository
}Meaning:
Whenever someone asks for UserRepository,
give UserRepositoryImpl.Use @Binds when you already have an implementation class with @Inject constructor.
Use @Provides when you need to manually build the object.
---
13. @Provides vs @Binds
| Point | @Provides | @Binds |
|---|---|---|
| Used for | Creating object manually | Interface to implementation mapping |
| Module type | Usually object | Usually abstract class |
| Function body | Has body | No body |
| Example | Retrofit, Room, OkHttp | Repository interface |
| Can create third-party object? | Yes | No |
Example:
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit.Builder().build()
}Example:
@Binds
abstract fun bindRepository(
impl: UserRepositoryImpl
): UserRepository---
14. Hilt scopes
Scope controls how long an object lives.
| Scope | Lifetime |
|---|---|
@Singleton | Whole app |
@ActivityScoped | Same Activity |
@FragmentScoped | Same Fragment |
@ViewModelScoped | Same ViewModel |
@ServiceScoped | Same Service |
Example:
@Provides
@Singleton
fun provideRetrofit(): RetrofitMeaning:
Only one Retrofit object for the whole app.Use @Singleton for:
Retrofit
OkHttpClient
Room database
Repository
SharedPreferences/DataStoreUse @ViewModelScoped when:
One object should be shared inside same ViewModel only.---
15. @InstallIn components
Common components:
| Component | Use |
|---|---|
SingletonComponent | App-level dependencies |
ActivityComponent | Activity-level dependencies |
FragmentComponent | Fragment-level dependencies |
ViewModelComponent | ViewModel-level dependencies |
ServiceComponent | Service-level dependencies |
Most common:
@InstallIn(SingletonComponent::class)Use it for app-wide dependencies.
---
16. Room Database with Hilt
Entity:
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: String,
val name: String
)DAO:
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun observeUsers(): Flow<List<UserEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(users: List<UserEntity>)
}Database:
@Database(
entities = [UserEntity::class],
version = 1
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}Hilt module:
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(
@ApplicationContext context: Context
): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
}
@Provides
fun provideUserDao(
database: AppDatabase
): UserDao {
return database.userDao()
}
}Meaning:
Hilt provides one Room database.
From that database, Hilt provides UserDao.---
17. Retrofit + Repository + ViewModel full flow
API
interface ProductApi {
@GET("products")
suspend fun getProducts(): List<ProductDto>
}Repository interface
interface ProductRepository {
suspend fun getProducts(): List<Product>
}Repository implementation
class ProductRepositoryImpl @Inject constructor(
private val api: ProductApi
) : ProductRepository {
override suspend fun getProducts(): List<Product> {
return api.getProducts()
.map { it.toDomain() }
}
}UseCase
class GetProductsUseCase @Inject constructor(
private val repository: ProductRepository
) {
suspend operator fun invoke(): List<Product> {
return repository.getProducts()
}
}ViewModel
@HiltViewModel
class ProductViewModel @Inject constructor(
private val getProductsUseCase: GetProductsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(ProductUiState())
val uiState = _uiState.asStateFlow()
fun loadProducts() {
viewModelScope.launch {
_uiState.value = ProductUiState(isLoading = true)
try {
val products = getProductsUseCase()
_uiState.value = ProductUiState(
products = products
)
} catch (e: Exception) {
_uiState.value = ProductUiState(
error = e.message
)
}
}
}
}Repository module
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindProductRepository(
impl: ProductRepositoryImpl
): ProductRepository
}Network module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideProductApi(
retrofit: Retrofit
): ProductApi {
return retrofit.create(ProductApi::class.java)
}
}Flow:
ProductScreen
|
v
ProductViewModel
|
v
GetProductsUseCase
|
v
ProductRepository interface
|
v
ProductRepositoryImpl
|
v
ProductApi / Retrofit---
18. Field injection
Sometimes Android framework classes need field injection.
Example:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var analytics: AnalyticsHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
analytics.logScreen("MainActivity")
}
}Use field injection mainly in Android classes:
Activity
Fragment
Service
BroadcastReceiverFor your own classes, prefer constructor injection.---
19. Inject Context
Use @ApplicationContext when you need app context.
class PreferenceManager @Inject constructor(
@ApplicationContext private val context: Context
)Use cases:
SharedPreferences
DataStore
Room database
File access
Application-level resource accessAvoid storing Activity context in singleton classes because it may cause memory leaks.
---
20. Qualifiers: when two objects have same type
Problem:
You need two Retrofit objects:
1. Auth API Retrofit
2. Payment API RetrofitHilt sees:
Retrofit
RetrofitIt gets confused.
Solution: use qualifiers.@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthRetrofit
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PaymentRetrofitModule:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@AuthRetrofit
@Provides
@Singleton
fun provideAuthRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://auth.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@PaymentRetrofit
@Provides
@Singleton
fun providePaymentRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://payment.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
fun provideAuthApi(
@AuthRetrofit retrofit: Retrofit
): AuthApi {
return retrofit.create(AuthApi::class.java)
}
@Provides
fun providePaymentApi(
@PaymentRetrofit retrofit: Retrofit
): PaymentApi {
return retrofit.create(PaymentApi::class.java)
}
}Meaning:
@AuthRetrofit tells Hilt: use auth Retrofit.
@PaymentRetrofit tells Hilt: use payment Retrofit.---
21. Hilt with Service
@AndroidEntryPoint
class UploadService : Service() {
@Inject
lateinit var uploadRepository: UploadRepository
override fun onStartCommand(
intent: Intent?,
flags: Int,
startId: Int
): Int {
// use uploadRepository
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
}Important:
Add @AndroidEntryPoint to Service if you want field injection.---
22. Hilt with WorkManager
For WorkManager, use Hilt Worker support.Dependencies:
implementation("androidx.hilt:hilt-work:1.2.0")
kapt("androidx.hilt:hilt-compiler:1.2.0")Worker:
@HiltWorker
class UploadWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val uploadRepository: UploadRepository
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
uploadRepository.uploadPendingFiles()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}Application:
@HiltAndroidApp
class MyApp : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}Meaning:
Hilt creates Worker and injects repository into it.---
23. Clean Architecture + Hilt structure
presentation
ProductScreen
ProductViewModel
domain
Product
ProductRepository
GetProductsUseCase
data
ProductRepositoryImpl
ProductApi
ProductDto
ProductMapper
di
NetworkModule
DatabaseModule
RepositoryModuleDependency direction:
Presentation -> Domain <- DataHilt connects the objects:
ViewModel needs UseCase
UseCase needs Repository interface
Repository interface bound to RepositoryImpl
RepositoryImpl needs Api/Dao
Api/Dao provided by Hilt modules---
24. Scenario-based examples
Scenario 1: ViewModel needs Repository
Use constructor injection:
@HiltViewModel
class LoginViewModel @Inject constructor(
private val repository: AuthRepository
) : ViewModel()---
Scenario 2: Repository needs Retrofit API
Use constructor injection in repository and @Provides for API:
class AuthRepositoryImpl @Inject constructor(
private val api: AuthApi
)---
Scenario 3: Interface has implementation
Use @Binds:
@Binds
abstract fun bindAuthRepository(
impl: AuthRepositoryImpl
): AuthRepository---
Scenario 4: Room Database needs Context
Use @Provides and @ApplicationContext:
@Provides
fun provideDatabase(
@ApplicationContext context: Context
): AppDatabase---
Scenario 5: Two API base URLs
Use qualifiers:
@AuthRetrofit
@PaymentRetrofit---
25. Common Hilt errors
Error 1: Missing @AndroidEntryPoint
Problem:
ViewModel injection not working in Activity/Fragment.Fix:
@AndroidEntryPoint
class MainActivity : AppCompatActivity()---
Error 2: Missing @HiltAndroidApp
Problem:
Hilt code generation not initialized.Fix:
@HiltAndroidApp
class MyApp : Application()---
Error 3: Interface cannot be provided
Problem:
Hilt does not know how to provide UserRepository.Fix:
@Binds
abstract fun bindUserRepository(
impl: UserRepositoryImpl
): UserRepository---
Error 4: Third-party class cannot be injected
Problem:
Retrofit cannot be created automatically.Fix:
@Provides
fun provideRetrofit(): Retrofit---
Error 5: Duplicate binding
Problem:
You provided same type two times.Fix:
Use qualifier annotations.---
26. Testing benefit
Without Hilt/DI:
ViewModel directly creates real repository.
Hard to test.With Hilt/DI:
ViewModel receives repository.
In test, pass FakeRepository.Example fake:
class FakeUserRepository : UserRepository {
override suspend fun getUser(): User {
return User(id = "1", name = "Test User")
}
}Now ViewModel test becomes easy because you can inject fake dependencies.
---
27. Interview answer
Hilt is a dependency injection library for Android built on top of Dagger. It reduces boilerplate and automatically provides dependencies to Android components such as Activity, Fragment, ViewModel, Service, and Worker. We start Hilt using @HiltAndroidApp in the Application class, use @AndroidEntryPoint for Android components, use @Inject for constructor injection, use @HiltViewModel for ViewModels, and use @Module with @Provides or @Binds for dependencies that Hilt cannot create automatically, such as Retrofit, Room, or interface implementations.
---
28. Best memory trick
@Inject:
"How to create this class."
@Module:
"Manual provider list."
@Provides:
"I will create object manually."
@Binds:
"When interface requested, use this implementation."
@Singleton:
"Create only one object for whole app."
@AndroidEntryPoint:
"This Android class can receive injection."
@HiltViewModel:
"Hilt will create this ViewModel."---
29. Final short summary
Hilt = automatic dependency provider for Android.
ViewModel needs UseCase.
UseCase needs Repository.
Repository needs API/DAO.
Hilt connects all these dependencies automatically.Best one-line answer:
Hilt makes Android code cleaner by creating and providing dependencies automatically, so classes focus on their own responsibility instead of manually creating other objects.