Koin — what it is, Dependency Injection principles and how it works

Author: IT Sectr Published: 2026-05-04 Reading time: 8 min

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 using DSL without reflection or code generation.
  • Module is a logical grouping of dependency registrations via single and factory functions.
  • single registers a singleton whose instance is created once.
  • factory creates a new instance on every request.
  • Scope binds dependency lifetime to a component, such as an Activity.

What is Koin and how is it different

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.

How the Koin container and DSL work

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.

startKoin and modules

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.

kotlin
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.

DSL and inline functions

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.

Dependency injection methods in Koin

Koin provides several ways to inject dependencies: by inject(), get(), and direct constructor passing. The choice depends on the usage context.

by inject() — lazy injection

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.

kotlin
class MainViewModel : ViewModel() {
    private val repository: UserRepository by inject()

    fun loadUsers() {
        repository.fetchAll()
    }
}

get() — explicit retrieval

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.

Modules and scopes in Koin

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.

scope — binding to a component

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.

kotlin
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 vs factory

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.

Koin in Android projects

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 { mockk() } registrations overrides the main module, and the ViewModel in the test gets predictable dependency behavior.

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

How is Koin different from Dagger Hilt?

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.

Does Koin support Kotlin Multiplatform?

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.

How to handle circular dependencies in Koin?

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.

Do I need to close scopes manually?

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.

How to test code with Koin?

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

  • Koin is a DI framework for Kotlin with DSL without reflection, annotations, or code generation.
  • Modules group registrations via single (singleton) and factory (new instance).
  • startKoin initializes a global container accessible via by inject() and get().
  • Scope binds dependency lifetime to the Android component lifecycle.
  • Injection in Compose is done via koinViewModel() and koinInject().
  • Koin supports Kotlin Multiplatform, allowing a single DI approach across all platforms.
  • DI errors are detected at runtime, so test coverage of the configuration is mandatory.

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.

Discuss the project

Read also