Dependency Injection
1. DI Fundamentals
Q: What is Dependency Injection and why use it?
Dependency Injection is a design pattern where objects receive their dependencies from external sources rather than creating them internally. Benefits:
- Testability: Swap real implementations with fakes/mocks
- Decoupling: Classes don't know how dependencies are created
- Reusability: Same class works with different implementations
- Maintainability: Change dependency configuration in one place
Q: What's the difference between DI and Service Locator?
| Dependency Injection | Service Locator |
|---|---|
| Dependencies pushed to object | Object pulls dependencies |
| Dependencies explicit in constructor | Dependencies hidden inside class |
| Compile-time verification | Runtime failures possible |
| Easier to test | Requires global state management |
Q: Constructor injection vs field injection?
Constructor injection (preferred): Dependencies are required parameters. Object can't exist without them. Immutable, testable, clear dependencies.
Field injection: Dependencies set after construction via annotation. Required for Android components (Activity, Fragment) where we don't control construction. Use sparingly elsewhere.
2. Hilt
Q: What is Hilt and how does it relate to Dagger?
Hilt is Google's DI library built on top of Dagger. It provides:
- Predefined components matching Android lifecycles
- Standard scopes (Singleton, ActivityScoped, etc.)
- Built-in support for ViewModel injection
- Less boilerplate than pure Dagger
Dagger is the underlying code generation engine; Hilt adds Android-specific conventions.
Q: What are the key Hilt annotations?
| Annotation | Purpose |
|---|---|
@HiltAndroidApp |
Trigger Hilt code generation on Application class |
@AndroidEntryPoint |
Enable injection in Activity, Fragment, Service, etc. |
@HiltViewModel |
Enable constructor injection for ViewModel |
@Inject |
Mark constructor for injection or field for field injection |
@Module |
Class that provides dependencies |
@InstallIn |
Specify which component the module belongs to |
@Provides |
Method that creates a dependency |
@Binds |
Bind interface to implementation (more efficient than @Provides) |
Q: Explain Hilt's component hierarchy.
SingletonComponent (Application lifetime)
↓
ActivityRetainedComponent (survives config changes)
↓
ViewModelComponent (ViewModel lifetime)
↓
ActivityComponent → FragmentComponent → ViewComponent
Objects in parent components can be injected into child components, but not vice versa.
3. Scopes
Q: What are scopes and why do they matter?
Scopes control the lifetime and sharing of dependencies. Without scoping, Hilt creates a new instance every time one is requested.
| Scope | Lifetime | Use Case |
|---|---|---|
@Singleton |
App lifetime | Retrofit, OkHttp, Database |
@ActivityRetainedScoped |
Survives rotation | Shared state across Activity recreations |
@ViewModelScoped |
ViewModel lifetime | Dependencies specific to one ViewModel |
@ActivityScoped |
Activity lifetime | Activity-specific presenters |
@FragmentScoped |
Fragment lifetime | Fragment-specific logic |
Q: What's the cost of @Singleton?
Singleton objects live for the entire app lifetime, consuming memory even when not needed. They can't be garbage collected. Use only for truly app-wide dependencies.
Q: How do scopes affect testing?
Scoped dependencies are shared within that scope. In tests, you may need to replace the entire scoped object, not just one usage. Hilt provides @TestInstallIn to swap modules for tests.
4. Providing Dependencies
Q: When do you use @Binds vs @Provides?
| @Binds | @Provides |
|---|---|
| Interface → Implementation mapping | Complex object creation |
| Abstract method | Concrete method |
| No method body | Method body with creation logic |
| More efficient (no extra method call) | Required for third-party classes |
Q: How do you provide third-party dependencies?
Use @Provides in a module since you can't add @Inject to classes you don't own:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.build()
}
Q: What are Qualifiers and when do you need them?
Qualifiers distinguish between multiple bindings of the same type. Example: You might have two OkHttpClient instances—one with auth interceptor, one without. Use custom qualifier annotations to differentiate.
5. ViewModel Injection
Q: How does ViewModel injection work in Hilt?
Mark ViewModel with @HiltViewModel and use @Inject constructor. Hilt creates a ViewModelProvider.Factory automatically. In UI, use by viewModels() (Fragment) or hiltViewModel() (Compose).
Q: How do you pass arguments to ViewModel via Hilt?
Use SavedStateHandle—it's automatically populated with arguments from the Fragment's bundle or Navigation arguments.
@HiltViewModel
class DetailViewModel @Inject constructor(
private val repository: Repository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val itemId: String = savedStateHandle.get<String>("itemId")!!
}
Q: How do you inject assisted dependencies (runtime values)?
Use @AssistedInject and @AssistedFactory. The factory takes runtime parameters that can't be provided by Hilt.
6. Testing with Hilt
Q: How do you test with Hilt?
- Use
@HiltAndroidTeston test class - Add
HiltAndroidRuleas a JUnit rule - Call
hiltRule.inject()before tests - Use
@TestInstallInto replace production modules
Q: How do you replace dependencies for testing?
Create a test module that replaces the production module:
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [RepositoryModule::class]
)
abstract class FakeRepositoryModule {
@Binds
abstract fun bind(impl: FakeRepository): Repository
}
Q: What about unit testing ViewModels?
For unit tests, skip Hilt entirely. Construct ViewModel directly with fake dependencies:
@Test
fun `test viewmodel`() {
val fakeRepository = FakeRepository()
val viewModel = MyViewModel(fakeRepository)
// test...
}
7. Common Patterns
Q: How do you handle optional dependencies?
Use @Nullable or provide a default. Hilt doesn't support optional bindings directly—every requested dependency must have a binding.
Q: How do you handle multibindings?
Hilt supports @IntoSet and @IntoMap for collecting multiple implementations:
@Provides
@IntoSet
fun provideInterceptor(): Interceptor = LoggingInterceptor()
All items annotated with @IntoSet for that type are collected into a Set<Interceptor>.
Q: How do you lazily initialize dependencies?
Inject Lazy<T> or Provider<T>:
Lazy<T>: Creates instance on first.get(), returns same instance thereafterProvider<T>: Creates new instance on every.get()call
Quick Reference
| Concept | Key Points |
|---|---|
| Hilt | Dagger + Android conventions; @HiltAndroidApp, @AndroidEntryPoint, @HiltViewModel |
| Scopes | @Singleton (app), @ViewModelScoped (VM), @ActivityScoped (activity); unscoped = new instance each time |
| @Binds | Interface → impl mapping; abstract, more efficient |
| @Provides | Complex creation, third-party classes; concrete method body |
| Qualifiers | Distinguish same-type bindings; custom annotations |
| Testing | @HiltAndroidTest, @TestInstallIn to swap modules; unit tests skip Hilt |