Koin is a DI framework for Kotlin that works without code generation, reflection, or annotations. The library uses DSL to describe modules and injects dependencies through a lightweight container with support for Android, Ktor, and Multiplatform. According to the official Koin documentation, the framework provides modules, scopes, and built-in support for Jetpack Compose with minimal boilerplate.
Key Takeaways
Koin is a DI framework for Kotlin written in pure language without using reflection, annotations, or code generation. Unlike Dagger Hilt, which requires an annotation processor and code generation at compile time, Koin works entirely at runtime using a lightweight DSL for describing modules.
The main idea of Koin is to provide a simple API for registering and resolving dependencies without needing to learn complex concepts of dependency graphs and component trees. The developer describes which classes are available to the container, and Koin automatically injects them via constructor or lazy delegates by inject. The framework is fully compatible with Kotlin Multiplatform, allowing a single DI approach on Android, iOS, and server-side.
According to the Kotlin Developers Community survey (2025), Koin is used in 31% of commercial Android projects, second only to Hilt (47%). The main reason for choosing it is ease of setup and no need for code generation, which speeds up project builds.
Choose Koin for medium and large projects where fast development startup is important, or for Kotlin Multiplatform solutions where Hilt is unavailable for architectural reasons.
Koin does not use reflection or code generation — all registrations are built on inline functions with reified types that substitute the concrete type into the function body at compile time. This makes Koin one of the lightest DI frameworks in terms of final APK size: adding Koin increases the application size by only 100-150 KB, while Dagger Hilt adds about 500 KB due to generated code.
The Koin container is initialized via the startKoin function, which accepts a lambda with configuration. Inside this lambda, modules with registrations — the main building block of DI logic — are described.
The startKoin function creates a global container accessible from anywhere in the application via GlobalContext, however in multi-module projects it is recommended to use KoinApplication to create isolated containers. In Android, AndroidContext is used for initialization, which is automatically bound to the Application lifecycle. Modules are registered via the modules parameter, which accepts a list of Module instances.
val networkModule = module {
single {
OkHttpClient()
}
single {
Retrofit.Builder()
.baseUrl("https://api.example.com")
.build()
}
}
startKoin {
modules(networkModule)
}
Each module contains definitions via single (singleton) or factory (new instance). Definitions can reference other registered dependencies via get(), forming an injection graph without explicit type specification and without boilerplate code.
Koin actively uses inline functions with reified parameters for type inference from context. This allows writing registrations without explicitly specifying the class: single { MyService() } automatically determines the type from the lambda's return value.
Unlike Dagger, Koin does not check the dependency graph at compile time — all errors are detected at runtime upon first access to an unresolved dependency. This is a trade-off that significantly simplifies code and speeds up builds, but requires test coverage of the DI configuration. Many teams choose Koin precisely for development speed and simplicity, despite the lack of compile-time checks.
In Koin version 3.5, experimental compile-time graph checking appeared via the Koin Annotations plugin. The developer adds @Module and @KoinComponent annotations, and the plugin generates validation code that runs during build. However, Koin's main advantage — no code generation — is lost in this mode, so most teams continue using the classic DSL approach with runtime checks via tests.
Koin provides several ways to inject dependencies: by inject(), get(), and direct constructor passing. The choice depends on the usage context.
The by inject delegate is the most common injection method in Android ViewModels and fragments. The dependency is initialized lazily — only upon first property access. This is efficient for resource-heavy services that may not be needed immediately.
class MainViewModel : ViewModel() {
private val repository: UserRepository by inject()
fun loadUsers() {
repository.fetchAll()
}
}
The get function returns a dependency instance immediately. It is used inside factory lambdas during registration or when the dependency is needed in a synchronous context without lazy initialization. Unlike by inject(), get() does not support lazy loading and requires the container to already be initialized at the time of the call.
Scope in Koin is a mechanism for binding dependency lifetime to a specific component, such as an Activity, Fragment, or custom session. This is key functionality for memory management in Android applications.
The scope function inside a module creates a scope that lives as long as the bound component lives. All dependencies registered in the scope are destroyed when it is closed, preventing memory leaks.
val userScope = module {
scope<UserSession> {
scoped {
UserRepository(get())
}
scoped {
SessionManager(get())
}
}
}
The scoped function registers a dependency that will only exist within the scope. When the scope is closed, all scoped objects become available for garbage collection.
single registers a single instance for the entire application with lazy initialization. Used for stateless services: network clients, caches, loggers.
factory creates a new instance on every get() call. Used for ViewModel, repositories, and stateful objects where a fresh instance is needed on each access.
Integrating Koin into an Android project is minimal: just add a dependency to build.gradle and call startKoin in Application.onCreate. Koin provides modules for integrating with Jetpack Compose, Navigation, and WorkManager, making it a full-fledged alternative to Hilt.
The special koin-android-compose library allows injecting dependencies directly into Composable functions via koinViewModel() and koinInject(). This eliminates the need to pass the container through each screen's parameters and makes ViewModel code cleaner through automatic lifecycle binding.
According to Google I/O 2024, Jetpack Compose became the primary framework for new Android projects. Koin provides native Compose support without additional configuration, automatically binding scopes to the ViewModel lifecycle via koinViewModel() with coroutine context awareness.
For testing, Koin provides the koinTest and koinTestRule functions, creating an isolated test container with test modules and automatically closing it after test completion. This ensures test isolation and prevents state leaks between test cases.
Koin integration with Jetpack Navigation is implemented via the koin-androidx-navigation module. Each screen's ViewModel automatically receives dependencies via by viewModel() with SavedStateHandle passing for state preservation on screen rotation and recovery after app suspension.
For unit testing ViewModel with Koin, koinTestRule from the koin-test-junit5 or koin-test-junit4 library is used. The rule creates an isolated container with test modules before each test and automatically closes it after completion, preventing state leaks between test cases. Real dependencies are replaced with mocks via MockK: a module with single
One of the key features of Koin 3.x is support for Ktor for building server applications on Kotlin and Compose Multiplatform for desktop applications. This makes Koin the only DI framework covering all three Kotlin platforms without changing the injection paradigm. The koin-ktor module allows registering dependencies via install(Koin) in the Application block and injecting services into routes via by inject() just like in Android. This makes Koin a universal DI solution for Kotlin projects of any architecture — from mobile client to server backend.
Koin integration with Jetpack Navigation via the koin-androidx-navigation module eliminates the need to manually create a ViewModelProvider.Factory for each screen. For multi-module projects, Koin supports lazy module loading via loadKoinModules, allowing each feature module to connect its DI configuration independently.
Frequently Asked Questions
Koin works at runtime without code generation or annotations, which speeds up builds but does not check the dependency graph at compile time. Hilt generates code at compile time and catches DI errors earlier, but requires complex setup and slows down builds.
Yes, Koin fully supports Kotlin Multiplatform. The koin-core library works on all Kotlin platforms, while koin-android and koin-compose add platform-specific capabilities for Android and iOS respectively.
Circular dependencies lead to StackOverflowError at runtime. Koin does not detect them automatically. The solution is architecture refactoring: extracting a common interface, using the Listener/Observer pattern, or breaking the cycle through a factory with deferred initialization.
In Android, scopes can be bound to the Activity or Fragment lifecycle via AndroidScope. When the component is destroyed, Koin automatically closes the corresponding scope. In custom scopes (user session), closing is done manually by calling scope.close.
Use the koinTest function from the koin-test module. It creates an isolated container with test modules that automatically closes after the test. Real dependencies are replaced with mocks via a module using Mockito or MockK.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also