For medium to large-scale apps, setting up a Dependency Injection (DI) framework early on
is extremely important for maintaining a well-organized and maintainable codebase.
If you’re unfamiliar with DI, here’s a relatable example.
Imagine you’re creating a Compose screen that displays a list of movies from a Room database.
In a larger app, we might organize the codebase to decouple concerns and improve maintainability.
For this, we’d typically need the following components:
- Database (): To store and retrieve movies.
- DAO: To define database operations.
- Repository: To abstract the data source and provide a clean API for data access.
- ViewModel: To manage UI logic and interact with the repository.
You may be interested in these articles as well:
Entity
The class represents a table in the Room database.
DAO
The interface defines the database operations for the entity.
Database
Define the Room database that provides the .
Repository
The repository abstracts the data source, making it easier to swap out Room with another data source in the future.
ViewModel
The manages UI-related data and interacts with the .
Moment of truth…
With everything at hand, we can now make the Compose screen.
And we need the instance here to show the list of movies and add new ones.
How do we get it?
See how challenging it is to get the ?. Without a DI framework, every screen
(e.g., ) must manually handle the initialization of its required dependencies,
like , , , and .
If you have many screens, this obviously becomes a nightmare.
With a Dependency Injection framework like this becomes as simple as:
All you need is a dependency graph like this
And then initialize Koin in the class.
Here’s how it works:
We ask to give the viewModel instance using :
Koin uses the type () to locate its declaration in the dependency graph,
and finds the following definition:
Here, Koin creates and returns a instance.
Since has a constructor parameter (), Koin resolves it by fetching
the necessary dependency. When is used, it tries to get the appropriate instance
based on the parameter type. In this case, it looks for a declaration and finds:
Koin applies the same process to create the instance, resolving its
dependencies as needed (e.g., the ). This chain continues until all required dependencies
are resolved and initialized automatically.
This could be the end of this tutorial, but
Here’s the thing though…
While the previous example demonstrates basic dependency injection, there’s a more robust way to implement it using interfaces and abstractions. This approach follows the Dependency Inversion Principle (DIP) from SOLID principles, which states that high-level modules should not depend on low-level modules, but both should depend on abstractions.
Let’s improve our implementation:
Then update our dependency declarations:
This approach offers several benefits:
- Loose Coupling: Components depend on abstractions rather than concrete implementations
- Testability: Easy to create mock implementations for testing
- Flexibility: Can swap implementations without changing dependent code
- Maintainability: Clear separation of concerns and dependencies
For example, if you later decide to switch from Room to a different database solution, you only need to:
- Create a new implementation of
- Update the dependency declaration in
The rest of your application code remains unchanged because it depends on the interface, not the implementation.
This pattern is particularly valuable in larger applications where you might need to:
- Switch between different data sources (local database vs remote API, SQLite vs PostgreSQL etc.)
- Create different implementations for different environments: development, testing, production. For example, you could use SQLite in development environment and PostgreSQL in production with minimal code changes.
- Mock dependencies for unit testing
- Gradually migrate from one implementation to another
Note: For the sake of easier comparison with the first example, prefixed interface names have been used
in the later example. While this is common in some languages, in Java/Kotlin, a preffered way is:
Interface: IMovieRepository -> MovieRepository
Implementation: MovieRepository -> MovieRepositoryImpl