Dependency Injection and Inversion of Control
Dependency Injection and Inversion of Control
Dependency Injection is a design pattern where an object receives its dependencies from an external source rather than creating them internally. This inverts the control of object creation, moving it from the consuming class to a separate component (the injector) that assembles the dependency graph. In Android development, DI frameworks like Dagger and Hilt automate this assembly at compile time, producing efficient code without the runtime reflection overhead of other approaches. By the end of this lesson, you will be able to:
- Explain the core principle of dependency injection and how it achieves loose coupling.
- Distinguish DI from the Factory pattern and the Service Locator pattern.
- Implement manual DI through constructor injection and understand its limitations at scale.
- Describe how Dagger generates dependency graphs at compile time.
- Explain how Hilt simplifies Dagger by providing predefined components and entry points.
The Core Principle
Without dependency injection, a class creates its own dependencies:
class UserRepository {
private val api = RetrofitClient.create()
private val db = AppDatabase.getInstance()
}
This code is difficult to test because you cannot substitute the real API client or database with test doubles. The UserRepository is tightly coupled to specific implementations, and changing the networking library requires modifying every class that creates its own client.
With dependency injection, the dependencies are provided externally through the constructor:
class UserRepository(
private val api: ApiService,
private val db: UserDao
) {
fun getUser(id: String): User = ...
}
Now the caller decides which ApiService and UserDao implementations to provide. In production, these are the real implementations. In tests, they are fakes or mocks. The UserRepository does not know or care where its dependencies come from.
DI vs Factory Pattern
The Factory pattern centralizes object creation in a factory class, but the client still requests the dependency explicitly:
class UserRepository {
private val api = ApiServiceFactory.create()
}
The client knows about the factory and calls it directly. This is better than direct instantiation because the factory can return different implementations based on configuration, but the client is still coupled to the factory class. You cannot substitute dependencies without changing the factory or introducing a factory interface.
With DI, the client has no knowledge of how dependencies are created. They simply appear in the constructor. The injector (whether manual code or a framework) is responsible for constructing the dependency graph, and the client is entirely passive in the process.
DI vs Service Locator
The Service Locator pattern provides a central registry that classes query for their dependencies:
This interview continues for subscribers
Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.
Become a Sponsor