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?

  1. Use @HiltAndroidTest on test class
  2. Add HiltAndroidRule as a JUnit rule
  3. Call hiltRule.inject() before tests
  4. Use @TestInstallIn to 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 thereafter
  • Provider<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

results matching ""

    No results matching ""