Dependency Injection
Hilt and Dagger dependency injection for managing object creation and scoping in Android apps
You are an expert in Hilt and Dagger dependency injection for building Android applications with Kotlin. ## Key Points - Use constructor injection wherever possible — it is the simplest and most testable approach. - Reserve `@Provides` for third-party classes and `@Binds` for mapping interfaces to implementations. - Scope dependencies only when truly necessary — unscoped bindings are created fresh each time, which is often fine and avoids memory leaks. - Inject `CoroutineDispatcher` via qualifiers instead of hardcoding `Dispatchers.IO` in classes, enabling testability. - Use `@ViewModelScoped` for dependencies shared between a ViewModel and its dependencies that should not outlive the ViewModel. - Prefer interface-based dependencies to enable easy faking in tests. - Forgetting `@AndroidEntryPoint` on an Activity or Fragment, causing a runtime crash when Hilt tries to inject. - Installing a module in the wrong component (e.g., `@InstallIn(ActivityComponent::class)` for a singleton), causing scoping errors. - Circular dependencies — Hilt throws a compile-time error. Break the cycle with `@Lazy` injection or redesign the dependency graph. - Using `@Singleton` on everything, preventing garbage collection and causing memory pressure. - Not annotating the ViewModel with `@HiltViewModel`, causing `hiltViewModel()` to fail at runtime. - Testing without `@UninstallModules` or `@BindValue`, accidentally using real implementations in unit tests. ## Quick Example ```kotlin @HiltAndroidApp class MyApplication : Application() ```
skilldb get android-kotlin-skills/Dependency InjectionFull skill: 268 linesDependency Injection — Android/Kotlin
You are an expert in Hilt and Dagger dependency injection for building Android applications with Kotlin.
Core Philosophy
Dependency injection in Android is about making the implicit explicit. When a class creates its own dependencies internally, those dependencies are hidden -- invisible to tests, invisible to callers, and impossible to swap for different environments. DI makes every dependency visible in the constructor, turning "this class secretly uses the real network" into "this class needs a UserRepository, and here is how to provide one." This transparency is the foundation of testable, modular code.
Hilt's value proposition is reducing the boilerplate of Dagger while keeping its compile-time safety. Dagger validates the entire dependency graph at compile time -- if a dependency is missing, you get a build error, not a runtime crash. Hilt layers Android-specific conventions on top: predefined components tied to Android lifecycle classes, automatic injection into Activities and Fragments, and first-class support for ViewModel injection. The trade-off is less flexibility than raw Dagger, but far less ceremony for the common cases.
Scoping is a tool, not a default. Making everything @Singleton seems safe but causes every object to live for the entire application lifetime, preventing garbage collection and increasing memory pressure. The right scope is the shortest lifetime that satisfies the dependency's needs. An API client that holds no mutable state can be unscoped (created fresh each time) without issue. A database instance should be @Singleton. A user session should be scoped to the authenticated portion of the app, not the entire application.
Anti-Patterns
-
Forgetting @AndroidEntryPoint on an Activity or Fragment: Without this annotation, Hilt does not know to inject dependencies into the class. The app compiles fine, but crashes at runtime when injection is attempted. This is the single most common Hilt setup mistake.
-
Making everything @Singleton: Scoping every dependency as a singleton prevents garbage collection of objects that are only needed temporarily, increases memory usage, and can lead to subtle bugs when stale state persists across different parts of the app. Scope only when the dependency truly needs a shared, long-lived instance.
-
Installing a module in the wrong component: Placing a ViewModel-scoped dependency in
SingletonComponentor a singleton dependency inActivityComponentcauses scoping errors or unexpected behavior. Match the module's@InstallInto the lifecycle that makes sense for its bindings. -
Circular dependencies in the dependency graph: When Service A depends on Service B which depends on Service A, Dagger throws a compile-time error. This usually signals a design problem. Break the cycle by introducing a protocol/interface at the boundary, splitting one of the services, or using
@Lazyinjection as a last resort. -
Using real implementations in unit tests instead of fakes: Without
@UninstallModulesor@BindValue, Hilt-injected tests use the production dependency graph, meaning tests hit the real network, real database, and real services. Always replace external dependencies with fakes or mocks in test configurations.
Overview
Hilt is the recommended dependency injection framework for Android, built on top of Dagger. It reduces boilerplate by providing predefined components tied to Android lifecycle classes (Application, Activity, ViewModel, etc.). Hilt generates the Dagger component hierarchy automatically, allowing you to focus on defining modules and injection targets.
Core Concepts
Application Setup
@HiltAndroidApp
class MyApplication : Application()
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme { AppNavHost() }
}
}
}
Modules and Provides
A module tells Hilt how to create instances of types that cannot be constructor-injected (interfaces, third-party classes).
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
Binds for Interfaces
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}
Constructor Injection
class UserRepositoryImpl @Inject constructor(
private val apiService: ApiService,
private val userDao: UserDao
) : UserRepository {
override fun observeUsers(): Flow<List<User>> =
userDao.observeAll().map { entities -> entities.map { it.toDomain() } }
override suspend fun refreshUsers() {
val users = apiService.getUsers()
userDao.upsertAll(users.map { it.toEntity() })
}
}
ViewModel Injection
@HiltViewModel
class UserListViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
val uiState: StateFlow<UserListUiState> = repository.observeUsers()
.map { UserListUiState.Success(it) as UserListUiState }
.catch { emit(UserListUiState.Error(it.message ?: "Unknown error")) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UserListUiState.Loading)
}
// In Compose
@Composable
fun UserListScreen(viewModel: UserListViewModel = hiltViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
// render state
}
Implementation Patterns
Scoping
Hilt provides predefined scopes tied to Android components:
| Scope | Component | Lifetime |
|---|---|---|
@Singleton | SingletonComponent | Application |
@ActivityScoped | ActivityComponent | Activity |
@ViewModelScoped | ViewModelComponent | ViewModel |
@FragmentScoped | FragmentComponent | Fragment |
Qualifiers
Use qualifiers to distinguish between multiple bindings of the same type.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DefaultDispatcher
@Module
@InstallIn(SingletonComponent::class)
object DispatcherModule {
@Provides
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@Provides
@DefaultDispatcher
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
}
class ImageProcessor @Inject constructor(
@DefaultDispatcher private val dispatcher: CoroutineDispatcher
) {
suspend fun process(bitmap: Bitmap): Bitmap = withContext(dispatcher) {
applyFilters(bitmap)
}
}
Assisted Inject
For classes that need both Hilt-provided and runtime parameters.
class PaymentProcessor @AssistedInject constructor(
private val apiService: ApiService,
@Assisted private val orderId: String
) {
suspend fun processPayment(): PaymentResult { /* ... */ }
@AssistedFactory
interface Factory {
fun create(orderId: String): PaymentProcessor
}
}
// Usage in ViewModel
@HiltViewModel
class CheckoutViewModel @Inject constructor(
private val paymentProcessorFactory: PaymentProcessor.Factory
) : ViewModel() {
fun checkout(orderId: String) {
viewModelScope.launch {
val processor = paymentProcessorFactory.create(orderId)
processor.processPayment()
}
}
}
Testing with Hilt
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class UserListScreenTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@BindValue
val fakeRepository: UserRepository = FakeUserRepository()
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun displaysUsers() {
// test with fakeRepository injected
}
}
Best Practices
- Use constructor injection wherever possible — it is the simplest and most testable approach.
- Reserve
@Providesfor third-party classes and@Bindsfor mapping interfaces to implementations. - Scope dependencies only when truly necessary — unscoped bindings are created fresh each time, which is often fine and avoids memory leaks.
- Inject
CoroutineDispatchervia qualifiers instead of hardcodingDispatchers.IOin classes, enabling testability. - Use
@ViewModelScopedfor dependencies shared between a ViewModel and its dependencies that should not outlive the ViewModel. - Prefer interface-based dependencies to enable easy faking in tests.
Common Pitfalls
- Forgetting
@AndroidEntryPointon an Activity or Fragment, causing a runtime crash when Hilt tries to inject. - Installing a module in the wrong component (e.g.,
@InstallIn(ActivityComponent::class)for a singleton), causing scoping errors. - Circular dependencies — Hilt throws a compile-time error. Break the cycle with
@Lazyinjection or redesign the dependency graph. - Using
@Singletonon everything, preventing garbage collection and causing memory pressure. - Not annotating the ViewModel with
@HiltViewModel, causinghiltViewModel()to fail at runtime. - Testing without
@UninstallModulesor@BindValue, accidentally using real implementations in unit tests.
Install this skill directly: skilldb add android-kotlin-skills
Related Skills
Architecture
MVVM architecture pattern with ViewModel, LiveData, and StateFlow for scalable Android apps
Coroutines
Kotlin coroutines and Flow for asynchronous programming and reactive streams in Android
Jetpack Compose
Jetpack Compose declarative UI toolkit for building native Android interfaces with Kotlin
Navigation
Jetpack Navigation component for type-safe in-app navigation and deep linking in Android
Retrofit
Retrofit HTTP client for type-safe REST API communication in Android with Kotlin coroutines
Room Database
Room persistence library for local SQLite database access with compile-time query verification in Android