Table of Contents
Understanding Dependency Injection
Dependency Injection, often abbreviated as DI, is a way to organize how different parts of your app get the objects they need to work. In Android apps, classes often depend on other classes. For example, a Repository might depend on a Dao, and a ViewModel might depend on a Repository. DI gives you a structured way to provide these dependencies instead of letting each class create everything by itself.
What Is a Dependency
A dependency is simply another object that a class needs in order to do its job. If LoginViewModel uses UserRepository, then UserRepository is a dependency of LoginViewModel.
Without thinking about DI, a typical class might look like this:
class LoginViewModel {
private val userRepository = UserRepository()
}
Here, LoginViewModel directly creates its dependency. This looks simple but becomes a problem when the dependency itself has more dependencies, or when you want to change or test the class.
With DI, you treat these needed objects as something that comes from the outside, not something the class creates by itself.
The Idea of Inversion of Control
Inversion of Control is a broad idea where an external component decides how and when your objects are created and connected. Dependency Injection is one way to apply Inversion of Control.
In a traditional design, a class controls its own dependencies by creating them. In an inverted design, something else creates those dependencies and passes them into the class. This gives more flexibility and reduces tight connections between classes.
Constructor Injection, Field Injection, and Method Injection
In DI, the way the dependency is given to an object is important. There are three main patterns.
Constructor injection gives the dependency through the class constructor:
class LoginViewModel(
private val userRepository: UserRepository
)
Here, LoginViewModel cannot be created without a UserRepository. This is the most common and recommended form of DI in Kotlin because it makes dependencies explicit and works well with immutability.
Field injection assigns the dependency directly to a property after the object is created. In Android DI frameworks, this often uses annotations on fields. This style is common for classes the system creates for you such as activities or fragments.
Method injection passes a dependency as a parameter to a function. This is useful when you need a dependency only inside a specific method, or when the dependency can change over time.
Constructor injection is usually the preferred way to inject dependencies in Kotlin, because it makes dependencies visible, encourages immutability, and works well with testing.
Why Dependency Injection Matters in Android
Android apps naturally grow into many layers, for example data sources, repositories, use cases, ViewModels, and UI components. If each class directly creates its own dependencies, the code becomes hard to change, hard to test, and hard to reuse.
With DI, you can:
Make classes easier to test. You can pass fake or mock implementations of dependencies instead of real ones.
Separate configuration from usage. One place decides which concrete classes are used, and the rest of the app just depends on interfaces or abstract types.
Reuse components. When classes are not tied to specific concrete implementations, you can reuse them in different parts of your app.
Manage scope and lifecycle of objects more clearly, which is especially important in Android where there are activities, fragments, and application lifecycles.
Coupling and Testability
When a class creates its own dependencies, it is tightly coupled to those specific implementations. If LoginViewModel always creates a real UserRepository, it is hard to run it in isolation with a different data source in tests.
With DI, you can define dependencies in terms of interfaces, then provide different implementations for production and for testing. For example, you can have UserRepository as an interface and then a FakeUserRepository for unit tests.
This approach encourages you to write code that depends on behavior, not on concrete classes. It also helps you write unit tests without needing a full Android environment.
Manual Dependency Injection
You can apply DI manually without using any framework. This is a useful starting point to understand the concept.
In manual DI, you create dependencies in a central place, then pass them into other objects. For example, you might have an AppContainer class that holds shared instances of your repositories and data sources.
class AppContainer {
val userRepository = UserRepository()
val loginViewModelFactory = LoginViewModelFactory(userRepository)
}
class LoginViewModel(
private val userRepository: UserRepository
)
class LoginViewModelFactory(
private val userRepository: UserRepository
) {
fun create(): LoginViewModel {
return LoginViewModel(userRepository)
}
}Your Android components, for example activities, can get a reference to this container from the application class and then use it to obtain what they need. This is still DI, just without an external library.
Manual DI is simple and transparent, but it can become verbose and harder to maintain as your app grows, especially when you need different object lifecycles.
Object Scopes and Lifecycles
In DI, scope refers to how long an object lives and who shares it. In an Android app you might want:
A single instance for the whole app lifetime, sometimes called an application scope. A typical example is a repository.
A single instance per activity or per screen. For example, a controller that manages UI state for that screen.
Short lived instances that are created when needed. For example, one object per network request.
Consistent management of scopes prevents resource leaks and keeps your app efficient. DI tools for Android are very focused on solving these scope and lifecycle problems, since Android components are created and destroyed frequently.
DI and Abstraction with Interfaces
DI encourages the use of abstractions such as interfaces. Instead of depending on RealUserRepository, a class can depend on UserRepository as an interface. This allows you to change or swap implementations without touching the classes that use them.
For example, you can have one implementation that uses a remote API and another that uses local storage. DI gives you a place to decide which one to create and inject based on build type, environment, or configuration.
This separation between interface and implementation is also what makes mocking and testing simpler.
DI Containers and Frameworks
Manually wiring dependencies works well for small apps or early learning. For larger apps, you often use a DI container or framework. A DI container is a tool that knows how to create and connect all your classes, following rules you define.
In Android, DI frameworks can handle:
Creating objects only when they are needed.
Reusing objects across the app or within specific scopes.
Injecting dependencies into Android classes such as activities and fragments that the system creates.
Managing complex dependency graphs so you do not have to manually construct long chains of objects.
You will later see how Hilt, a DI framework built on top of Dagger and designed for Android, makes this process easier and more automatic while still applying all the concepts described here.
Recognizing When to Use DI
You know you can benefit from DI when:
Your constructors have many parameters and you pass them through several layers.
Your unit tests are hard to write because you cannot replace dependencies easily.
Changing one implementation requires you to modify many other classes.
The same type of object is created in many different places in your app.
Applying DI concepts helps simplify these situations by centralizing object creation and making your classes focus on logic instead of configuration.