Dependency Injection
Dependency injection with Dagger 2 — components, modules, bindings, and multibinding.
4 questions
JuniorTheoryVery commonWhat does a dependency-injection framework like Hilt add over manual constructor injection?
What does a dependency-injection framework like Hilt add over manual constructor injection?
Passing a class its dependencies through the constructor is DI itself — that already gives you decoupling and a fake in tests, with no library at all. A framework such as Hilt adds the wiring: it generates the object graph, resolves transitive dependencies and manages scopes, so you stop maintaining factories by hand. Manual DI is fine while the graph is small.
Common mistakes
- ✗Thinking manual constructor injection is not DI and that a framework is what makes it one
- ✗Believing
Hilt/Daggerresolve the graph by runtime reflection rather than generating code - ✗Reaching for a DI framework on a tiny graph where a hand-written factory is simpler
Follow-up questions
- →What is a scope in the DI framework
Hilt, and what does a@Singletonbinding actually guarantee? - →How do you substitute a fake for a real dependency in a test when there is no DI framework?
JuniorTheoryCommonIn the DI framework Dagger 2, what is the difference between @Component and @Module?
In the DI framework Dagger 2, what is the difference between @Component and @Module?
A @Module is a class that supplies dependencies — its @Provides methods describe HOW to create objects. A @Component is the interface Dagger implements: it wires the listed modules into a dependency graph and exposes the objects you can request (the injection entry point).
Common mistakes
- ✗Swapping the roles — thinking the
@Componentprovides objects and the@Moduleexposes them - ✗Believing Dagger resolves the graph by reflection at runtime rather than generating code at compile time
- ✗Assuming a
@Moduleworks without being listed in a@Component
Follow-up questions
- →How does a
@Componentknow which modules to use? - →What does Dagger generate from a
@Componentinterface at compile time?
MiddleTheoryCommonIn Dagger 2, how do @Binds and @Provides differ, and what is multibinding for?
In Dagger 2, how do @Binds and @Provides differ, and what is multibinding for?
@Provides is a concrete factory method whose body builds the object; @Binds is an abstract method that just maps an interface to an existing implementation, so Dagger generates less code and skips the no-op factory. Multibinding (@IntoSet/@IntoMap) collects many bindings into one injectable Set/Map.
Common mistakes
- ✗Writing a
@Providesbody that just returns its parameter, where a@Bindsis the leaner fit - ✗Marking a
@Bindsmethod concrete instead of abstract - ✗Thinking multibinding overrides bindings rather than aggregating them into a
Set/Map
Follow-up questions
- →Why must a
@Bindsmethod be abstract and live in an abstract module? - →When would you reach for
@IntoMapover@IntoSet?
SeniorDesignRareA Kotlin Multiplatform codebase keeps its domain and data layers in commonMain and ships them to an Android app, an iOS app and a JVM backend. Constraints:
- The dependency graph must be declared once, in shared code — not copied into each platform module.
- Some bindings are platform-specific (an Android Context-backed database driver, an iOS keychain, a JVM connection pool) and must be supplied per target.
- The iOS app is written in Swift and must obtain the same objects from the shared module.
- The annotation-processor DI framework Dagger/Hilt runs only on the JVM, so it cannot build the graph for the Kotlin/Native iOS target.
- Tests must be able to swap a real dependency for a fake on any target.
Describe how you would keep DI consistent across all three targets — where the graph lives, how platform-specific bindings enter it, how the Swift side reaches it, and what you give up compared with a compile-time-verified graph.
A Kotlin Multiplatform codebase keeps its domain and data layers in commonMain and ships them to an Android app, an iOS app and a JVM backend. Constraints:
- The dependency graph must be declared once, in shared code — not copied into each platform module.
- Some bindings are platform-specific (an Android Context-backed database driver, an iOS keychain, a JVM connection pool) and must be supplied per target.
- The iOS app is written in Swift and must obtain the same objects from the shared module.
- The annotation-processor DI framework Dagger/Hilt runs only on the JVM, so it cannot build the graph for the Kotlin/Native iOS target.
- Tests must be able to swap a real dependency for a fake on any target.
Describe how you would keep DI consistent across all three targets — where the graph lives, how platform-specific bindings enter it, how the Swift side reaches it, and what you give up compared with a compile-time-verified graph.
Declare the graph once in commonMain with a multiplatform DI library such as Koin, which resolves at runtime and needs no JVM annotation processor. Supply platform-specific bindings from expect/actual modules into that shared graph; expose one initializer Android and Swift both call. You give up Dagger's compile-time graph validation, so add a test that resolves every binding.
Common mistakes
- ✗Assuming an annotation-processor DI framework can generate a graph for the Kotlin/Native target
- ✗Duplicating the object graph per platform instead of declaring it once in shared code
- ✗Forgetting that a runtime-resolved graph fails at first use, not at build time, unless a test resolves it
Follow-up questions
- →How do
expect/actualdeclarations feed a platform-specific binding into the shared graph? - →How would you stop the runtime DI library
Koinfrom failing at first use rather than at startup?