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 Repository

Diagram:

MainActivity
    |
    v
UserViewModel
    |
    v
UserRepository
    |
    +--> UserApi
    |
    +--> UserDao

---

6. Important Hilt annotations

AnnotationMeaning
@HiltAndroidAppStarts Hilt in Application class
@AndroidEntryPointAllows injection in Activity/Fragment/Service
@InjectTells Hilt how to create object or inject field
@HiltViewModelAllows Hilt to create ViewModel
@ModuleClass/object that provides dependencies
@InstallInDefines where dependency lives
@ProvidesManually create and provide object
@BindsBind interface to implementation
@SingletonOne object for whole app
@ApplicationContextProvides application context
@ActivityContextProvides 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
Interfaces
For 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 forCreating object manuallyInterface to implementation mapping
Module typeUsually objectUsually abstract class
Function bodyHas bodyNo body
ExampleRetrofit, Room, OkHttpRepository interface
Can create third-party object?YesNo

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.

ScopeLifetime
@SingletonWhole app
@ActivityScopedSame Activity
@FragmentScopedSame Fragment
@ViewModelScopedSame ViewModel
@ServiceScopedSame Service

Example:

@Provides
@Singleton
fun provideRetrofit(): Retrofit

Meaning:

Only one Retrofit object for the whole app.

Use @Singleton for:

Retrofit
OkHttpClient
Room database
Repository
SharedPreferences/DataStore

Use @ViewModelScoped when:

One object should be shared inside same ViewModel only.

---

15. @InstallIn components

Common components:

ComponentUse
SingletonComponentApp-level dependencies
ActivityComponentActivity-level dependencies
FragmentComponentFragment-level dependencies
ViewModelComponentViewModel-level dependencies
ServiceComponentService-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
BroadcastReceiver
For 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 access

Avoid 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 Retrofit

Hilt sees:

Retrofit
Retrofit

It gets confused.

Solution: use qualifiers.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthRetrofit

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PaymentRetrofit

Module:

@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
    RepositoryModule

Dependency direction:

Presentation -> Domain <- Data

Hilt 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.