Hilt Dependency Injection: Complete Android DI Guide
Hilt is Google's recommended dependency injection library for Android, built on top of Dagger. It simplifies DI setup by providing predefined components and container bindings.
Setup and Installation
Add Hilt to your project:
# build.gradle.kts (project)
plugins {
id("com.google.dagger.hilt.android") version "2.48" apply false
}
# build.gradle.kts (app)
plugins {
kotlin("kapt")
id("com.google.dagger.hilt.android")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
}
HiltAndroidApp: Application Setup
Every Hilt project needs an Application class annotated with @HiltAndroidApp:
@HiltAndroidApp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Hilt initializes automatically
}
}
This generates the DI container and must be declared in AndroidManifest.xml:
<application
android:name=".MyApplication"
...>
</application>
AndroidEntryPoint: Inject Dependencies
Mark Android classes for dependency injection:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: TaskViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// viewModel is automatically injected
}
}
@AndroidEntryPoint
class TaskFragment : Fragment() {
private val viewModel: TaskViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// viewModel is automatically injected
}
}
Supported classes: Activity, Fragment, View, Service, BroadcastReceiver.
Modules: Define Dependencies
Use @Module and @InstallIn to define how to create dependencies:
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
@Singleton
fun provideDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app.db"
).build()
}
@Provides
@Singleton
fun provideTaskDao(database: AppDatabase): TaskDao {
return database.taskDao()
}
@Provides
@Singleton
fun provideTaskRepository(dao: TaskDao): TaskRepository {
return TaskRepository(dao)
}
}
Binds for Interfaces
Use @Binds to provide interface implementations:
interface TaskDataSource {
suspend fun getTasks(): List<TaskEntity>
}
class LocalTaskDataSource(
private val dao: TaskDao
) : TaskDataSource {
override suspend fun getTasks(): List<TaskEntity> {
return dao.getAllTasks()
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class DataSourceModule {
@Binds
@Singleton
abstract fun bindTaskDataSource(
impl: LocalTaskDataSource
): TaskDataSource
}
@Binds is more efficient than @Provides for interface bindings.
HiltViewModel and hiltViewModel()
Inject dependencies into ViewModels:
@HiltViewModel
class TaskViewModel @Inject constructor(
private val repository: TaskRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
val tasks: StateFlow<List<TaskEntity>> = repository.allTasks
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = emptyList()
)
fun addTask(title: String) {
viewModelScope.launch {
repository.addTask(TaskEntity(title = title))
}
}
}
In Compose, use hiltViewModel():
@Composable
fun TaskScreen(
viewModel: TaskViewModel = hiltViewModel()
) {
val tasks by viewModel.tasks.collectAsState()
LazyColumn {
items(tasks) { task ->
TaskRow(task)
}
}
}
Scopes: Component Lifetimes
Hilt provides predefined scopes for different component lifetimes:
// SingletonComponent - lives for app lifetime
@Provides
@Singleton
fun provideAppManager(context: Context): AppManager {
return AppManager(context)
}
// ViewModelComponent - lives for ViewModel lifetime
@Provides
@ViewModelScoped
fun provideTaskInteractor(repository: TaskRepository): TaskInteractor {
return TaskInteractor(repository)
}
// ActivityComponent - lives for Activity lifetime
@Provides
@ActivityScoped
fun provideActivityLogger(): Logger {
return Logger("Activity")
}
// FragmentComponent - lives for Fragment lifetime
@Provides
@FragmentScoped
fun provideFragmentLogger(): Logger {
return Logger("Fragment")
}
Common scopes:
-
@Singleton(SingletonComponent) -
@ViewModelScoped(ViewModelComponent) -
@ActivityScoped(ActivityComponent) -
@FragmentScoped(FragmentComponent)
Qualifiers: Disambiguate Same Types
When multiple implementations exist for the same type, use qualifiers:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LocalDataSource
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class RemoteDataSource
@Module
@InstallIn(SingletonComponent::class)
object DataSourceModule {
@Provides
@Singleton
@LocalDataSource
fun provideLocalDataSource(dao: TaskDao): TaskDataSource {
return LocalTaskDataSource(dao)
}
@Provides
@Singleton
@RemoteDataSource
fun provideRemoteDataSource(): TaskDataSource {
return RemoteTaskDataSource()
}
}
@HiltViewModel
class TaskViewModel @Inject constructor(
@LocalDataSource private val local: TaskDataSource,
@RemoteDataSource private val remote: TaskDataSource
) : ViewModel() {
// Both implementations injected with different qualifiers
}
Best Practices
- Use @HiltAndroidApp: Every app needs exactly one
- @AndroidEntryPoint on UI Classes: Makes injection seamless
- @singleton Carefully: Only use for true app-wide singletons
- @ViewModelScoped for Interactors: Align lifecycle with ViewModel
- @Binds for Interfaces: More efficient than @Provides
- Qualifiers for Disambiguation: Clear and type-safe
- Keep Modules Organized: Group related providers
-
Test with Testing Components: Use
@UninstallModulesfor tests
8 Android App Templates → https://myougatheaxo.gumroad.com
Top comments (0)