Are you tired of the complexity of dependency injection? Koin is revolutionizing Kotlin development with a lightweight and pragmatic approach. It allows you to organize your project with elegant syntax and minimal configuration. Discover how this framework can simplify your code while giving you full control over your dependencies.
What is Dependency Injection?
Dependency injection (DI) is an application design pattern used in object-oriented programming to facilitate the creation and management of object instances in an application. Instead of having an object create its own dependencies (the instances of other objects it needs to function), the dependencies are provided externally—typically through configuration or direct injection.
This approach offers several benefits:
- Decoupling: objects don't need to know the implementation details of their dependencies. This reduces class coupling, making the code more flexible, maintainable, and easier to test.
- Reusability: by passing dependencies as parameters, objects become more reusable in different contexts. This promotes modularity and makes it easier to build scalable applications.
- Facilitates unit testing: by easily providing mocked dependencies during testing, DI enables the creation of effective and robust unit tests.
One last point worth noting is that DI is closely related to the SOLID principles that should be applied in software development.
The Birth of Koin
Koin creators Arnaud Giuliani and Laurent Broudoux aimed to solve several specific problems they encountered with existing dependency injection solutions in the Java/Kotlin ecosystem:
- Complexity and overhead. While frameworks like Dagger2 and Hilt are powerful, they can also be complex to configure and use. They require a lot of boilerplate code, code generation at compile time, and have a significant learning curve—especially for small or medium projects.
- Performance. Code generation and reflection (commonly used in other frameworks) can negatively impact application performance, particularly on resource-constrained devices like mobile phones. Koin was designed to be lightweight and efficient, avoiding code generation and minimizing reflection usage.
- Ease of use and learning. Koin was proposed as a simpler, more intuitive alternative, with a concise and easy-to-understand API. The goal was to allow development teams to quickly start using dependency injection without having to deal with complex setups.
- Integration with Kotlin. Koin was specifically designed for Kotlin, leveraging language features like extension functions and lambdas to provide a more fluent and expressive syntax.
- Testing. Koin makes unit testing easier by allowing for the injection of mocks or stubs in a straightforward way.
In summary, the creators of Koin were aiming for a dependency injection solution that is:
- Lightweight: with minimal impact on application performance.
- Simple: easy to use and learn, with a concise API.
- Intuitive: with a syntax that integrates naturally with Kotlin.
- Code-generation-free: to avoid long compile times.
- Test-friendly: to support the creation of effective unit tests.
What Makes Koin So Successful Among Development Teams?
There are several reasons why Koin stands out. First, its declarative approach and simple syntax make it extremely easy to understand and use. Instead of relying on XML configurations or complex configuration classes, Koin uses DSLs (Domain-Specific Languages) in Kotlin to define our components and their dependencies in a concise and readable way.
Additionally, Koin is extremely lightweight and has a minimal footprint in our applications, meaning it doesn’t significantly increase the size of the app or impact its performance. This makes it ideal for both small apps and large-scale projects.
Another noteworthy feature of Koin is its seamless integration with the Android ecosystem, providing specific support for Android. It simplifies dependency injection in activities, fragments, services, and other Android components. It also fully supports the use of Jetpack Compose.
Moreover, the library is multiplatform, and can be used across iOS, web, and JVM environments.
Last but not least, Koin encourages a decoupled and modular architecture, which facilitates unit testing and code refactoring. This means we can write more effective tests and keep our code cleaner and more maintainable over time.
Service Locator
One important point to understand about Koin is its initial nature.
Primarily, it resembles a Service Locator more than a traditional dependency injector. This might seem surprising given that it's commonly used for dependency injection, but the difference between libraries like Dagger2 or Hilt and Koin lies in the fact that the former are "pure" DI frameworks, as they generate Java code at compile-time to provide dependency instances. At compile-time, you can know if something was incorrectly declared or missing.
Koin centralizes dependency configuration in a specific place. It uses a component called a module where all dependencies and their providers are defined. When a component requires a dependency, it requests it from Koin, which acts as a service container. Koin looks into its registry and provides the appropriate instance of the requested service. This is why we can’t know in advance if something is missing—it will fail at runtime.
To address this, there are tasks and tools that can perform a compile-time sweep to detect potential misconfigurations.
Getting Started with Koin
Project Configuration
To start using Koin in your project, the first step is to import the necessary libraries.

These libraries form the entire Koin ecosystem, but you won’t need all of them initially. Start by configuring the basics and add more as your needs grow.
The first thing we need to do is configure Gradle, and since Koin uses a BOM (Bill of Materials) for its libraries (starting from version 3.5.0), it’s easy—we just need to add koin-bom and koin-core to our application:

However, the best practice is to use version catalogs, like this:

And in the dependencies section:

This way, it will always be updated to the version defined in koin-bom.
Android Configuration
To use Koin on Android, we'll need a few additional libraries. The first one is koin-android:

You can also configure this via BOM. Now we can load Koin into our application like this:

We might also need some additional features in our app, such as:

How Do We Start Using It in Android?
Let’s consider a typical Android project setup where we have an Application class (where we’ll initialize Koin), one or more Activities, multiple Fragments (if any), ViewModels (if using MVVM), and potentially use cases, repositories, datastore, Room database, remote service connections, etc.
In such a project, you’ll clearly need to use koin-android, and that’s how you’ll begin to define your project’s DI setup. We’ll create a directory called DI, where we’ll place all the files containing Koin injection modules we’ll need (sometimes one module is enough, other times it’s better to split into several if there's a lot to inject).

Inside that AppModule.kt, we’ll have something like this:
val appModule = module {
}
Here we’ll be adding our dependency or object injection definitions that we need, using the DSL provided by Koin.
Let’s suppose we have a data class defined like this:
data class Film(
val characters: List<String>,
val director: String,
val title: String
)
interface StarWarsRepository {
suspend fun getAllfilms(): Result<List<Film>>
}
The class that implements this repository would look something like this:
class StarWarsRepositoryImpl(
private val starWarsApiService: StarWarsApiService
): StarWarsRepository
{
override suspend fun getAllfilms(): Result<List<Film>> = tryCall {
starWarsApiService.getAllfilms().map { it.map() }
}
}
Since this is a repository, it would be a singleton and therefore defined in the AppModule.kt file as follows:
val appModule = module {
single<StarWarsRepository> { StarWarsRepositoryImpl(get()) }
}
StarWarsApiService would be the service—such as Retrofit—that makes the call to the server to retrieve the data, and it would also be defined in DI.
That is, the word ‘single’ in Koin tells us that this class will be a singleton, and that every time someone requests it, they’ll get the same instance of the object that was created the first time (as long as it hasn’t been destroyed).
So, what that line means is that if someone requests an object of type StarWarsRepository, Koin will return an object of that type using the StarWarsRepositoryImpl class.
If we had a use case, we could define it as follows:
interface GetAllFilmsUseCase {
suspend operator fun invoke(): Result<List<Film>>
}
class GetAllFilmsUseCaseImpl(
private val repository: StarWarsRepository
) : GetAllFilmsUseCase
{
override suspend operator fun invoke(): Result<List<Film>> = repository.getAllfilms()
}
AppModule.kt would look like this:
val appModule = module {
factory<GetAllFilmsUseCase> { GetAllFilmsUseCaseImpl(get()) }
single<StarWarsRepository> { StarWarsRepositoryImpl(get()) }
}
In this case, we can see that use cases are typically defined as ‘factory’. This means they will be generated as many times as needed, every time they’re requested—although, in reality, many use cases are unique and could perfectly be defined as single.
We also see that the implementation of the use case requires a parameter, which happens to be of type StarWarsRepository. This is where Koin works its magic: when an object is requested, it detects that the GetAllFilmsUseCaseImpl class needs a parameter and, as indicated there, it uses get(). That get() tells Koin to look through the rest of the definitions for an object of the corresponding class. That’s when it creates the StarWarsRepository class (defined as a single) and injects it as a parameter into the use case.
We need to understand that we must include as many get() calls as there are parameters needed to instantiate the corresponding class.
If we now have a ViewModel that uses that use case, then we would have something like this:
class FilmsViewModel(
private val getAllFilmsUseCase: GetAllFilmsUseCase
) : ViewModel()
{
………
}
And the definition would be:
val appModule = module {
viewModel { FilmsViewModel(get()) }
factory<GetAllFilmsUseCase> { GetAllFilmsUseCaseImpl(get()) }
single<StarWarsRepository> { StarWarsRepositoryImpl(get()) }
}
In this case, the word viewModel is responsible for indicating that it should be injected in a specific way when requested. To request an object of the FilmsViewModel class, we would do it as follows:
class MainActivity : AppCompatActivity() {
private val viewModel: FilmsViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//...
}
}
That by viewModel() is what will locate an object of type FilmsViewModel and inject it directly into that Activity.
Clearly, Koin will locate objects if they already exist (if they are singletons), and if they don’t, it will instantiate them. That’s why sometimes, when trying to access an object, the app crashes at runtime—caused by a misconfiguration in the module file for the objects that should be created.
In the case of a Fragment, everything would be done the same way as in the Activity example in order to access the corresponding ViewModel.
Lastly, our application class would look like this:
class CustomApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@CustomApplication)
modules(
listOf( //listOf por si hay mas de un modulo.
appModule
)
)
}
}
}
We would be defining a logger so that Koin sends traces to logcat, and we would also be indicating to Koin that the context to use in the container is the application's own context.
To wrap up this first look at Koin
Koin offers a refreshing alternative in the world of dependency injection for Kotlin developers. Its minimalist approach eliminates the need for complex annotations and code generation, allowing you to focus on what really matters: building your app. Throughout this journey, we've seen how its intuitive syntax and ease of implementation can transform your project architecture.
As developers, we value tools that simplify our work without sacrificing power. Koin proves that dependency injection doesn’t need to be complicated to be effective. If you’re looking for a pragmatic, 100% Kotlin-based solution, Koin definitely deserves a place in your projects.
Recommended links
Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.
Tell us what you think.