Irbisa · Cheatsheet

Android Dependency Injection

Hilt, Dagger, Koin, scopes, modules, testability

Middle Developer · 24 questions

  1. 01

    What is dependency injection, and how does it relate to Dependency Inversion, IoC and a service locator?

    Dependency injection means a class receives its dependencies from outside, usually through the constructor, instead of creating them or fetching them from a global.

    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()…
  2. 02

    Constructor or field injection: which do you prefer, and why does Hilt fall back to field injection in Activities and Fragments?

    Constructor injection is the default because the object cannot exist without its dependencies; Activities and Fragments are created by the system through a no-arg constructor, so Hilt has to assign @Inject fields after creation instead.

    @AndroidEntryPointclass MainActivity : AppCompatActivity() {   @Inject lateinit var analytics: AnalyticsTracker   // field injection, not private  private val vm: MainViewModel by viewModels()      // built by Hilt's factory…
  3. 03

    What do @Inject, @Module, @Provides and @Component do in Dagger, and when is the graph actually built?

    @Inject marks constructors Dagger may call, @Module with @Provides builds types it cannot construct itself, and @Component is the interface Dagger implements to connect them; the graph is validated at compile time, instances are created lazily at runtime.

    class UserRepository @Inject constructor(private val api: UserApi) @Moduleobject NetworkModule {  @Provides @Singleton  fun retrofit(): Retrofit =…
  4. 04

    What is Hilt, and what does it add on top of Dagger 2?

    Hilt is Google's DI library for Android: Dagger 2 with a predefined component tree tied to Android lifecycles, so you annotate classes instead of writing and wiring components yourself.

    @HiltAndroidAppclass MyApp : Application() @HiltViewModelclass HomeViewModel @Inject constructor(  private val repo: UserRepository,…
  5. 05

    When do you use @Binds instead of @Provides, and can both live in one module?

    @Binds tells Dagger that one type is satisfied by an existing binding of another type, so it generates nothing but an alias; @Provides runs a function body you wrote.

    interface AnalyticsTracker { fun log(event: String) } class FirebaseTracker @Inject constructor(  private val firebase: FirebaseAnalytics) : AnalyticsTracker {  override fun log(event: String) = firebase.logEvent(event, null)…
  6. 06

    How do Hilt scopes map to component lifetimes, and why shouldn't you scope everything?

    A scope makes a binding one instance per instance of its component, so picking a scope is a claim about how long the object lives; unscoped bindings are built fresh at every injection point, and that is the right default.

  7. 07

    The Memory Profiler shows three live OkHttpClient instances, 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)

    The @Provides method has no scope, so it is a recipe rather than an instance: Dagger runs it again for every injection point, and three consumers get three clients.

  8. 08

    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 }}

    @ViewModelScoped means one instance per ViewModel, not one shared between ViewModels: each @HiltViewModel gets its own ViewModelComponent, so ProductViewModel and CartBadgeViewModel hold two different CartState objects and the badge watches the empty one.

  9. 09

    This @HiltViewModel asks for an @ActivityContext. What happens when you build it, and which Context should 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)}

    The build fails with [Dagger/MissingBinding]: ViewModelComponent sits under ActivityRetainedComponent, which outlives any single Activity, so there is no @ActivityContext to give it.

  10. 10

    What are @Qualifier and multibindings (@IntoSet, @IntoMap) for, with a practical example?

    A @Qualifier tells Dagger which of several bindings of the same type you mean, and multibinding collects many bindings into one Set or Map.

    @Qualifier @Retention(AnnotationRetention.BINARY)annotation class AuthClient @Module@InstallIn(SingletonComponent::class)object NetworkModule {…
  11. 11

    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?

    A hard-coded dispatcher is a dependency in disguise: inject a CoroutineDispatcher behind a qualifier so the test can pass a TestDispatcher and control time.

    @Qualifier @Retention(AnnotationRetention.BINARY) annotation class IoDispatcher@Qualifier @Retention(AnnotationRetention.BINARY) annotation class ApplicationScope @Module@InstallIn(SingletonComponent::class)object CoroutinesModule {…
  12. 12

    A detail screen's ViewModel needs the orderId it was opened with. How does a runtime value get into an injected class?

    A value known only at runtime cannot be a binding, so it arrives through SavedStateHandle or through an assisted factory.

    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)…
  13. 13

    What do Provider<T> and Lazy<T> give you over injecting T directly?

    Both defer construction until you call get(); Provider resolves the binding on every call, while Lazy builds once on first call and caches the result.

    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…
  14. 14

    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()  )}

    AuthInterceptor reads AuthToken once, in its constructor, and the @Singleton OkHttpClient builds that interceptor once.

  15. 15

    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)}

    The cycle is OkHttpClient -> AuthInterceptor -> TokenRefresher -> AuthApi -> OkHttpClient; break it by giving the refresh AuthApi its own qualified client without the auth interceptor, not by hiding the loop behind Provider.

  16. 16

    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?

    Hilt has a dedicated integration for Workers and an @EntryPoint escape hatch for everything else.

    @HiltWorkerclass SyncWorker @AssistedInject constructor(  @Assisted appContext: Context,  @Assisted params: WorkerParameters,  private val repo: FeedRepository) : CoroutineWorker(appContext, params) {…
  17. 17

    The build fails with [Dagger/MissingBinding] for PaymentGateway. How do you read the error and find the cause?

    Dagger reached a type it has no recipe for, and the request chain printed under the message shows which component asked for it and through whom.

    // 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.…
  18. 18

    How do you replace dependencies with fakes in tests when the app uses Hilt?

    In unit tests you skip Hilt and call constructors with fakes; in instrumented and Robolectric tests you use @HiltAndroidTest with @TestInstallIn, @UninstallModules or @BindValue to swap bindings.

    // Test source set: replaces the real module in every Hilt test@Module@TestInstallIn(  components = [SingletonComponent::class],  replaces = [NetworkModule::class])…
  19. 19

    Hilt or Koin: what actually differs, and when would you choose each?

    Hilt builds and checks the whole graph at compile time; Koin resolves dependencies at runtime from a registry, which is simpler to set up but moves a missing binding from the build to the running app.

  20. 20

    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?

    Dagger writes a factory per binding and the component implementations, and the Hilt Gradle plugin rewrites each @AndroidEntryPoint class to extend a generated Hilt_ base class that performs the injection.

    // You write:class AnalyticsTracker @Inject constructor(client: OkHttpClient) @AndroidEntryPointclass MainActivity : AppCompatActivity() {  @Inject lateinit var analytics: AnalyticsTracker…
  21. 21

    Subcomponents or component dependencies: what differs under the hood, and which fits a multi-module app?

    A subcomponent is generated inside its parent and inherits every parent binding; a component dependency is a separately generated component that sees only what the parent explicitly exposes through provision methods.

    // 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 }}…
  22. 22

    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?

    A @Singleton outlives the user session: the repository that cached user A's orders is the same object user B gets, because nothing in the graph is tied to being logged in.

    @Scope @Retention(AnnotationRetention.RUNTIME) annotation class SessionScope @SessionScope@DefineComponent(parent = SingletonComponent::class)interface SessionComponent…
  23. 23

    In a Gradle project with forty modules, where does Hilt actually assemble the graph, and what does that do to build times?

    Hilt collects every @InstallIn module and entry point on the app's classpath and generates all components once, in the application module, which is why that module becomes the build bottleneck as the graph grows.

  24. 24

    What does Dagger cost a large Kotlin codebase, and why are teams evaluating Metro or kotlin-inject in 2026?

    Dagger's costs are build time, a Java-centred code generator and an Android-only Hilt layer; Metro (a Kotlin compiler plugin) and kotlin-inject (KSP) keep compile-time checking while running inside the Kotlin toolchain and supporting Multiplatform.