Irbisa · Cheatsheet
Android Dependency Injection
Hilt, Dagger, Koin, scopes, modules, testability
What is dependency injection, and how does it relate to Dependency Inversion, IoC and a service locator?
class UserRepository(private val api: UserApi, private val dao: UserDao)class UserViewModel(private val repo: UserRepository) : ViewModel() // Composition root: the one place that knows how to build everythingclass AppContainer(context: Context) { private val retrofit = Retrofit.Builder().baseUrl("https://api.example.com/").build()…Constructor or field injection: which do you prefer, and why does Hilt fall back to field injection in Activities and Fragments?
@AndroidEntryPointclass MainActivity : AppCompatActivity() { @Inject lateinit var analytics: AnalyticsTracker // field injection, not private private val vm: MainViewModel by viewModels() // built by Hilt's factory…What do
@Inject,@Module,@Providesand@Componentdo in Dagger, and when is the graph actually built?class UserRepository @Inject constructor(private val api: UserApi) @Moduleobject NetworkModule { @Provides @Singleton fun retrofit(): Retrofit =…What is Hilt, and what does it add on top of Dagger 2?
@HiltAndroidAppclass MyApp : Application() @HiltViewModelclass HomeViewModel @Inject constructor( private val repo: UserRepository,…When do you use
@Bindsinstead of@Provides, and can both live in one module?interface AnalyticsTracker { fun log(event: String) } class FirebaseTracker @Inject constructor( private val firebase: FirebaseAnalytics) : AnalyticsTracker { override fun log(event: String) = firebase.logEvent(event, null)…How do Hilt scopes map to component lifetimes, and why shouldn't you scope everything?
The Memory Profiler shows three live
OkHttpClientinstances, yet this module provides only one. What went wrong?@Module@InstallIn(SingletonComponent::class)object NetworkModule { @Provides fun okHttp(auth: AuthInterceptor): OkHttpClient = OkHttpClient.Builder().addInterceptor(auth).build()} class FeedApiClient @Inject constructor(private val client: OkHttpClient)class ImageLoader @Inject constructor(private val client: OkHttpClient)class UploadService @Inject constructor(private val client: OkHttpClient)Find the bug: the product screen adds an item to the cart, but the cart badge in the toolbar stays at 0.
@ViewModelScopedclass CartState @Inject constructor() { private val _items = MutableStateFlow<List<Item>>(emptyList()) val items: StateFlow<List<Item>> = _items.asStateFlow() fun add(item: Item) = _items.update { it + item }} @HiltViewModelclass ProductViewModel @Inject constructor(private val cart: CartState) : ViewModel() { fun addToCart(item: Item) = cart.add(item)} @HiltViewModelclass CartBadgeViewModel @Inject constructor(cart: CartState) : ViewModel() { val count: Flow<Int> = cart.items.map { it.size }}This
@HiltViewModelasks for an@ActivityContext. What happens when you build it, and whichContextshould it take?@HiltViewModelclass ProfileViewModel @Inject constructor( private val repo: ProfileRepository, @ActivityContext private val context: Context) : ViewModel() { private val prefs = context.getSharedPreferences("profile", Context.MODE_PRIVATE)}What are
@Qualifierand multibindings (@IntoSet,@IntoMap) for, with a practical example?@Qualifier @Retention(AnnotationRetention.BINARY)annotation class AuthClient @Module@InstallIn(SingletonComponent::class)object NetworkModule {…A repository test burns real seconds and times out on slow CI because the repository hard-codes
Dispatchers.IO. How do you restructure it with DI?@Qualifier @Retention(AnnotationRetention.BINARY) annotation class IoDispatcher@Qualifier @Retention(AnnotationRetention.BINARY) annotation class ApplicationScope @Module@InstallIn(SingletonComponent::class)object CoroutinesModule {…A detail screen's ViewModel needs the
orderIdit was opened with. How does a runtime value get into an injected class?class OrderLoader @AssistedInject constructor( @Assisted private val orderId: String, // runtime value private val repo: OrderRepository // from the graph) { suspend fun load(): Order = repo.load(orderId)…What do
Provider<T>andLazy<T>give you over injectingTdirectly?class ReportGenerator @Inject constructor( private val db: Lazy<AppDatabase>, // dagger.Lazy: opened on first get(), then cached private val sessions: Provider<Session> // a fresh Session on every get()) { fun run(): Report { val dao = db.get().reports() // the database opens only if run() is called…Find the bug: after the access token is refreshed, every request still goes out with the old one until the app restarts.
@Module@InstallIn(SingletonComponent::class)object NetworkModule { @Provides fun authToken(store: TokenStore): AuthToken = store.current() @Provides @Singleton fun client(auth: AuthInterceptor): OkHttpClient = OkHttpClient.Builder().addInterceptor(auth).build()} class AuthInterceptor @Inject constructor(private val token: AuthToken) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response = chain.proceed( chain.request().newBuilder().header("Authorization", "Bearer ${token.value}").build() )}This network module fails with
[Dagger/DependencyCycle]. Where is the cycle, and what is the right way to break it?class AuthInterceptor @Inject constructor( private val tokens: TokenRefresher) : Interceptor { /* adds the Authorization header */ } class TokenRefresher @Inject constructor(private val api: AuthApi) @Module@InstallIn(SingletonComponent::class)object NetworkModule { @Provides @Singleton fun client(auth: AuthInterceptor): OkHttpClient = OkHttpClient.Builder().addInterceptor(auth).build() @Provides @Singleton fun authApi(client: OkHttpClient): AuthApi = Retrofit.Builder().baseUrl("https://api.example.com/").client(client).build().create(AuthApi::class.java)}How do you inject into a class Hilt does not own, such as a WorkManager
Worker, a ContentProvider, or an object a third-party library constructs?@HiltWorkerclass SyncWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted params: WorkerParameters, private val repo: FeedRepository) : CoroutineWorker(appContext, params) {…The build fails with
[Dagger/MissingBinding]forPaymentGateway. How do you read the error and find the cause?// Request chain, trimmed:// PaymentGateway is injected at CheckoutRepository(gateway, ...)// CheckoutRepository is injected at CheckoutViewModel(repo, ...)// CheckoutViewModel is requested at App_HiltComponents.ViewModelC// Bottom-up: ViewModelC asked, via CheckoutViewModel -> CheckoutRepository.…How do you replace dependencies with fakes in tests when the app uses Hilt?
// Test source set: replaces the real module in every Hilt test@Module@TestInstallIn( components = [SingletonComponent::class], replaces = [NetworkModule::class])…Hilt or Koin: what actually differs, and when would you choose each?
You never wrote a class called
Hilt_MainActivity, yet it shows up in stack traces. What do Hilt and Dagger actually generate at build time?// You write:class AnalyticsTracker @Inject constructor(client: OkHttpClient) @AndroidEntryPointclass MainActivity : AppCompatActivity() { @Inject lateinit var analytics: AnalyticsTracker…Subcomponents or component dependencies: what differs under the hood, and which fits a multi-module app?
// Subcomponent: generated inside the parent, sees all its bindings@Subcomponent(modules = [CheckoutModule::class])interface CheckoutComponent { fun inject(fragment: CheckoutFragment) @Subcomponent.Factory interface Factory { fun create(): CheckoutComponent }}…After logging out and signing in as a different user, the app briefly shows the previous user's orders. What does the DI graph have to do with it?
@Scope @Retention(AnnotationRetention.RUNTIME) annotation class SessionScope @SessionScope@DefineComponent(parent = SingletonComponent::class)interface SessionComponent…In a Gradle project with forty modules, where does Hilt actually assemble the graph, and what does that do to build times?
What does Dagger cost a large Kotlin codebase, and why are teams evaluating Metro or kotlin-inject in 2026?