Hilt Dependency Injection on Android: Setup, Scopes, and Multi-Binding for View and Compose
Hilt's version sensitivity remains a time sink that AI assistants often get wrong, making a verified version tuple the fastest path to a working project. Understanding that `@Binds` and `@Provides` cannot coexist in one module prevents a cryptic KSP compilation error that otherwise stalls new adopters.
Getting a minimal Hilt project to compile still trips up developers who lean on AI instead of the official docs. The root cause is almost always a version mismatch between Kotlin, KSP, and Hilt. A working combination is Kotlin 2.0.21, KSP 2.0.21-1.0.28, and Hilt 2.57, with the `hilt-android-compiler` dependency routed through `ksp` rather than the deprecated `kapt`.
Once the build is stable, Hilt usage breaks down into three injection strategies. Constructor injection with `@Inject` works for classes you own. Interface injection requires an abstract `@Module` with `@Binds` to map an implementation to its contract. Third-party classes that can't have their constructors modified rely on `@Provides` inside a concrete `@Module`. A common compile-time trap is mixing `@Binds` and `@Provides` in the same module, which Hilt forbids.
Scope management follows a strict parent-child component hierarchy: `SingletonComponent` outlives the app process, `ActivityRetainedComponent` survives configuration changes, and `ActivityComponent` is destroyed on rotation. For injecting multiple instances of the same class, custom `@Qualifier` annotations let you tag both the provider functions and the injection sites, and those same qualifiers can be applied to function parameters when one injected object depends on a specific variant of another.
AI coding assistants frequently hallucinate Hilt version combinations, making the official documentation the only reliable source for a clean project setup.
The prohibition against mixing `@Binds` and `@Provides` in one module is a practical constraint that forces a clean separation between interface bindings and factory methods, which aligns with good modular design even if it initially feels like boilerplate.
Using `@Qualifier` on function parameters, not just on fields, is an under-documented pattern that lets a provider function declare exactly which variant of a dependency it needs, keeping the resolution logic inside the DI graph rather than in business code.