Android Dependency Injection
Irbisa · cheatsheetSeptember 13, 2026

Android Dependency Injection

Hilt, Dagger, Koin, scopes, modules, testability

Middle Developer20 itemscompressed for a skim
  1. 01

    What is dependency injection and why use it on Android?

    Easy

    Dependency injection (DI) means giving an object its collaborators from outside, rather than letting it construct or look them up itself.

    // Manual DI — fine for small projects
    class UserRepo(private val api: UserApi, private val dao: UserDao)
    class UserViewModel(private val repo: UserRepo) : ViewModel()
    
    // Composition root in MyApp.onCreate
    class MyApp : Application() {
    …
  2. 02

    What is Hilt? How does it differ from Dagger 2?

    Medium

    Hilt is Google's official DI library for Android, and it is Dagger 2 with the component tree already written for you.

    // Application
    @HiltAndroidApp
    class MyApp : Application()
    
    // Module — provide bindings
    @Module
    …
  3. 03

    What are scopes in Hilt and how do they map to component lifecycles?

    Medium

    A Hilt scope pins a binding to the lifetime of one Android component, from the whole application down to a single ViewModel.

    @Module
    @InstallIn(SingletonComponent::class)
    object AppModule {
      @Provides @Singleton
      fun provideAnalyticsClient(): AnalyticsClient = ProdAnalyticsClient()
    }
    …
  4. 04

    What are @Qualifier and multibinding in Dagger/Hilt?

    Medium

    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
    @Qualifier @Retention(AnnotationRetention.BINARY)
    annotation class AuthClient
    @Qualifier @Retention(AnnotationRetention.BINARY)
    annotation class IoDispatcher
    …
  5. 05

    How do you replace dependencies in tests with Hilt?

    Medium

    Hilt provides the @HiltAndroidTest rule and @TestInstallIn / @UninstallModules to swap real bindings for fakes in instrumented tests.

    // Production
    @Module
    @InstallIn(SingletonComponent::class)
    object NetworkModule {
      @Provides @Singleton fun provideUserApi(): UserApi = realApi
    }
    …
  6. 06

    Hilt vs Koin — when to choose which?

    Medium

    Hilt buys compile-time verification of the whole object graph; Koin buys a plain Kotlin DSL and no code generation.

    // Hilt — annotation-driven
    @HiltViewModel
    class MyVM @Inject constructor(private val repo: Repo) : ViewModel()
    
    @Module
    @InstallIn(SingletonComponent::class)
    …
  7. 07

    When do you use @Binds instead of @Provides?

    Medium

    @Binds says one type is satisfied by an existing binding of another type, so Dagger generates nothing but an alias; @Provides runs a method 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)
    …
  8. 08

    The system creates Activities and Fragments, so Hilt cannot use their constructors. How does it inject them?

    Easy

    Hilt falls back to field injection for classes the framework instantiates, and @AndroidEntryPoint is what makes it happen.

    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
    
      @Inject lateinit var analytics: AnalyticsTracker   // not private
      private val vm: MainViewModel by viewModels()      // @HiltViewModel
    …
  9. 09

    A detail screen's ViewModel needs the orderId it was opened with. Where does that value come from, given the DI graph has never heard of it?

    Hard

    A value known only at runtime cannot be a binding, so it either arrives through SavedStateHandle or is handed in by 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)
    …
  10. 10

    What do Provider<T> and Lazy<T> give you, and how does one of them break a dependency cycle?

    Medium

    Both defer construction until you call get(); Provider hands you a new instance every call, Lazy builds once and caches.

    class ReportGenerator @Inject constructor(
      private val db: Lazy<AppDatabase>,             // opened on first get(), then cached
      private val sessions: Provider<Session>        // a fresh Session per call
    ) {
      fun run(): Report {
        val dao = db.get().reports()                 // DB opened only if run() is called
    …
  11. 11

    How do you inject into a class Hilt does not own — a WorkManager Worker, a ContentProvider, or an object a third-party library constructs?

    Medium

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

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

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

    Hard

    Hilt aggregates every @InstallIn module in the whole project and generates the components once, in the app module — which is exactly why that module becomes the bottleneck.

    interface OrderRepository { suspend fun load(id: String): Order }   // :feature:orders:api
    
    @Module                                     // :feature:orders:impl
    @InstallIn(SingletonComponent::class)
    abstract class OrderModule {
      @Binds abstract fun bindRepo(impl: OrderRepositoryImpl): OrderRepository
    …
  13. 13

    A @Singleton thumbnail cache needs a Context — which one does Hilt hand you, and what breaks if you keep an Activity's?

    Easy

    Hilt binds two of them behind qualifiers — @ApplicationContext lives as long as the process, @ActivityContext dies with the Activity — and only the first one is legal inside a @Singleton.

    // WRONG — the graph is fine, the leak is hand-written
    @Singleton
    class ThumbnailCache @Inject constructor() {
      private var context: Context? = null
      fun attach(activity: Activity) { context = activity }   // retained for the whole process
    }
    …
  14. 14

    A repository test passes on your laptop and times out on CI; the repository calls withContext(Dispatchers.IO) itself — what do you change?

    Easy

    Hard-coded dispatchers are dependencies in disguise: inject a CoroutineDispatcher behind a qualifier so a test can hand the repository 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 {
    …
  15. 15

    The build fails with [Dagger/MissingBinding] PaymentGateway cannot be provided without an @Provides-annotated method — how do you read that?

    Medium

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

    // The error, trimmed:
    //
    // [Dagger/MissingBinding] PaymentGateway cannot be provided without an
    // @Provides-annotated method.
    //     PaymentGateway is injected at
    //         CheckoutRepository(gateway, ...)
    …
  16. 16

    Two composable destinations in one NavHost each call hiltViewModel<CartViewModel>() and get different instances — what decides that?

    Medium

    hiltViewModel() resolves the nearest ViewModelStoreOwner from LocalViewModelStoreOwner, and inside a NavHost each destination's NavBackStackEntry is its own owner — so one instance per destination, cleared when that entry is popped.

    @HiltViewModel
    class CartViewModel @Inject constructor(
      private val cart: CartRepository,
      private val state: SavedStateHandle
    ) : ViewModel()
    …
  17. 17

    A Koin app crashes with NoDefinitionFoundException on a screen QA rarely opens — how do you turn that into a build or test failure?

    Medium

    Koin resolves at runtime by type key, so a missing definition only surfaces when that code path runs — the fix is to verify the modules in a test, or move to Koin Annotations and let KSP check at compile time.

    val dataModule = module {
      singleOf(::AuthInterceptor)
      single { OkHttpClient.Builder().addInterceptor(get<AuthInterceptor>()).build() }
      singleOf(::OrderRepositoryImpl) { bind<OrderRepository>() }
      factoryOf(::OrderFormatter)        // a new instance per get()
      viewModelOf(::OrderViewModel)      // SavedStateHandle comes from the platform
    …
  18. 18

    The Memory Profiler shows nine live OkHttpClient instances in an app whose module provides exactly one — what went wrong?

    Medium

    An unscoped binding is a recipe, not an instance: Dagger runs the @Provides method again for every injection point, so nine consumers get nine clients.

    @Module
    @InstallIn(SingletonComponent::class)
    object NetworkModule {
    
      // WRONG (before) — no scope, so Dagger re-runs this for every injection point
      // @Provides
    …
  19. 19

    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?

    Hard

    Effectively everything: Dagger writes a factory per binding and the component implementations, and the Hilt Gradle plugin rewrites each @AndroidEntryPoint class's superclass to a generated Hilt_ base that performs the injection.

    // You write:
    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
      @Inject lateinit var analytics: AnalyticsTracker
    }
    …
  20. 20

    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?

    Hard

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

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