Kotlin Multiplatform Dependency Injection
Source Project
You can find the Kotlin Multiplatform project here: https://github.com/InsertKoinIO/hello-kmp
Platform Support
Koin Core supports all Kotlin Multiplatform targets:
| Platform | Target | Koin Artifact | Notes |
|---|---|---|---|
| Android | JVM/Android | koin-android | Full Android integration (Activity, Fragment, ViewModel, etc.) |
| iOS | Native (ARM64, X64, Simulator) | koin-core | KoinComponent pattern for Swift interop |
| Desktop | JVM | koin-core | Works with Compose Desktop |
| Web | JS, WASM | koin-core | Experimental WASM support |
| Server | JVM, Native | koin-core | Use with Ktor, Spring, etc. |
| Linux/Windows/macOS | Native | koin-core | Native targets supported |
Gradle Setup
Using Version Catalogs (Recommended)
In your gradle/libs.versions.toml:
[versions]
koin = "4.2.0"
[libraries]
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
In your shared/build.gradle.kts:
kotlin {
// Your targets configuration
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
// Desktop, Web, etc.
sourceSets {
commonMain.dependencies {
implementation(libs.koin.core)
}
commonTest.dependencies {
implementation(libs.koin.test)
}
androidMain.dependencies {
implementation(libs.koin.android)
// For Compose on Android
implementation(libs.koin.compose)
}
}
}
For Compose Multiplatform integration, see Koin Compose.
Shared Koin Module
Platform specific components can be declared here, and be used later in Android or iOS (declared directly with actual classes or even actual module)
You can find the shared module sources here: https://github.com/InsertKoinIO/hello-kmp/tree/main/shared
// platform Module
val platformModule = module {
singleOf(::Platform)
}
// KMP Class Definition
expect class Platform() {
val name: String
}
// iOS
actual class Platform actual constructor() {
actual val name: String =
UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
// Android
actual class Platform actual constructor() {
actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}
Koin modules need to be gathered via a function:
// Common App Definitions
fun appModule() = listOf(commonModule, platformModule)
expect/actual Patterns with Koin
Kotlin Multiplatform provides two main patterns for platform-specific code: expect/actual and interfaces. Here's when to use each with Koin.
Pattern 1: expect/actual Classes (Platform-Specific Implementation)
Use when you need platform-specific APIs (Android Context, iOS UIDevice, etc.):
// commonMain - Declaration
expect class PlatformContext
expect fun createPlatformModule(): Module
// androidMain - Android Implementation
actual class PlatformContext(val context: Context)
actual fun createPlatformModule() = module {
single { PlatformContext(androidContext()) }
}
// iosMain - iOS Implementation
actual class PlatformContext
actual fun createPlatformModule() = module {
single { PlatformContext() }
}
Pattern 2: Interface + Platform Implementations (Dependency Injection)
Use when you want to inject different implementations per platform:
// commonMain - Interface & Module
interface Logger {
fun log(message: String)
}
val commonModule = module {
// Will be provided by platform modules
}
// androidMain - Android Implementation
class AndroidLogger : Logger {
override fun log(message: String) {
android.util.Log.d("App", message)
}
}
val androidModule = module {
single<Logger> { AndroidLogger() }
}
// iosMain - iOS Implementation
class IOSLogger : Logger {
override fun log(message: String) {
println("iOS: $message")
}
}
val iosModule = module {
single<Logger> { IOSLogger() }
}
Pattern 3: Hybrid - expect Module, actual Implementations
Best for complex platform modules:
// commonMain
expect val platformModule: Module
// androidMain
actual val platformModule = module {
single<Logger> { AndroidLogger() }
single { AndroidHttpClient() }
single { PlatformContext(androidContext()) }
}
// iosMain
actual val platformModule = module {
single<Logger> { IOSLogger() }
single { IOSHttpClient() }
single { PlatformContext() }
}
When to use which pattern:
- expect/actual classes: Platform APIs (Context, UIDevice), simple platform differences
- Interfaces: Business logic that varies by platform, testable code
- expect modules: Complex platform-specific dependency graphs
Android Context in Shared Code
A common need is accessing Android Context in shared code. Here's the recommended pattern:
ContextWrapper Pattern
// commonMain - Wrapper interface
interface AppContext
// androidMain - Android implementation
class AndroidAppContext(val context: Context) : AppContext
val androidContextModule = module {
single<AppContext> { AndroidAppContext(androidContext()) }
}
// iosMain - Empty implementation
class IOSAppContext : AppContext
val iosContextModule = module {
single<AppContext> { IOSAppContext() }
}
Usage in shared code:
// commonMain - Repository can access platform context
class FileRepository(private val appContext: AppContext) {
fun saveFile(data: String) {
when (appContext) {
is AndroidAppContext -> {
// Android-specific file operations
val file = File(appContext.context.filesDir, "data.txt")
file.writeText(data)
}
is IOSAppContext -> {
// iOS-specific file operations (if needed)
}
}
}
}
val sharedModule = module {
single { FileRepository(get()) }
}
For pure shared logic, prefer abstracting platform operations into interfaces rather than using when statements. The above is for cases where platform-specific APIs are necessary.
Architecture Patterns in KMP
Repository Pattern
// commonMain - Domain layer
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun saveUser(user: User)
}
val domainModule = module {
// Repository implementation provided by platform or data module
}
// commonMain - Data layer (can be in separate sourceSet)
class UserRepositoryImpl(
private val api: UserApi,
private val database: UserDatabase
) : UserRepository {
override suspend fun getUser(id: String): User {
return try {
api.fetchUser(id).also { database.saveUser(it) }
} catch (e: Exception) {
database.getUser(id)
}
}
override suspend fun saveUser(user: User) {
database.saveUser(user)
api.updateUser(user)
}
}
val dataModule = module {
single<UserRepository> { UserRepositoryImpl(get(), get()) }
}
Network Layer (Ktor + Koin)
// commonMain
val networkModule = module {
single {
HttpClient {
install(ContentNegotiation) {
json()
}
}
}
single { UserApi(get()) }
}
class UserApi(private val client: HttpClient) {
suspend fun fetchUser(id: String): User {
return client.get("https://api.example.com/users/$id").body()
}
}
Database Layer (Example with SqlDelight)
// commonMain
expect class DriverFactory {
fun createDriver(): SqlDriver
}
val databaseModule = module {
single { DriverFactory().createDriver() }
single { AppDatabase(get()) }
single { get<AppDatabase>().userQueries }
}
// androidMain
actual class DriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}
}
// iosMain
actual class DriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(AppDatabase.Schema, "app.db")
}
}
ViewModel Pattern in KMP
// commonMain - Shared ViewModel (using koin-core-viewmodel or custom)
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_user.value = repository.getUser(id)
}
}
}
val viewModelModule = module {
// For Compose Multiplatform
viewModel { UserViewModel(get()) }
// Or for Android-specific
// viewModel { params -> UserViewModel(get(), params.get()) }
}
Module Organization Strategies
By Layer (Clean Architecture)
// commonMain
val dataModule = module {
single<UserRepository> { UserRepositoryImpl(get(), get()) }
single { UserApi(get()) }
single { UserDatabase(get()) }
}
val domainModule = module {
factory { GetUserUseCase(get()) }
factory { SaveUserUseCase(get()) }
}
val presentationModule = module {
viewModel { UserViewModel(get(), get()) }
}
// Combine all modules
fun appModules() = listOf(
networkModule,
databaseModule,
dataModule,
domainModule,
presentationModule,
platformModule
)
By Feature
// Feature 1: Authentication
val authModule = module {
single<AuthRepository> { AuthRepositoryImpl(get()) }
factory { LoginUseCase(get()) }
viewModel { LoginViewModel(get()) }
}
// Feature 2: Profile
val profileModule = module {
single<ProfileRepository> { ProfileRepositoryImpl(get()) }
factory { GetProfileUseCase(get()) }
viewModel { ProfileViewModel(get()) }
}
// Platform-specific features
expect val authPlatformModule: Module
fun appModules() = listOf(
authModule,
profileModule,
authPlatformModule
)
Testing KMP Modules
Unit Testing Shared Modules
// commonTest
class UserRepositoryTest : KoinTest {
@Test
fun testGetUser() = runTest {
startKoin {
modules(module {
single<UserApi> { FakeUserApi() }
single<UserDatabase> { FakeUserDatabase() }
single<UserRepository> { UserRepositoryImpl(get(), get()) }
})
}
val repository: UserRepository = get()
val user = repository.getUser("123")
assertEquals("John", user.name)
stopKoin()
}
}
Testing with Platform-Specific Dependencies
// commonTest
expect class TestPlatformContext
// androidTest
actual class TestPlatformContext(val context: Context)
// iosTest
actual class TestPlatformContext
// commonTest - Test module
class PlatformDependentTest : KoinTest {
@Test
fun testWithPlatformContext() {
startKoin {
modules(module {
single { TestPlatformContext() }
single { MyService(get()) }
})
}
val service: MyService = get()
// Test service that depends on platform context
stopKoin()
}
}
Common Pitfalls & Best Practices
✅ DO: Use interfaces for testable shared code
// Good - Testable
interface Logger {
fun log(message: String)
}
val sharedModule = module {
single { UserService(get<Logger>()) }
}
❌ DON'T: Use expect classes for business logic
// Bad - Hard to test, tight platform coupling
expect class Logger {
fun log(message: String)
}
✅ DO: Keep platform modules separate
// Good - Clear separation
fun initKoin() {
startKoin {
modules(commonModules() + platformModule)
}
}
❌ DON'T: Mix platform-specific code in shared modules
// Bad - Platform-specific code in commonMain
val sharedModule = module {
single {
if (Platform.isAndroid) { /* ... */ } // Don't do this!
}
}
✅ DO: Use lazy modules for large apps
// Good - Optimize startup
val lazyFeatureModule = lazyModule {
// Heavy dependencies loaded on-demand
}
startKoin {
modules(coreModules)
lazyModules(lazyFeatureModule)
}
❌ DON'T: Forget to close scopes
// Bad - Memory leak
class FeatureScreen : KoinComponent {
val scope = getKoin().createScope<FeatureScreen>()
// Forgot to close scope!
}
// Good - Proper cleanup
class FeatureScreen : KoinComponent {
val scope = getKoin().createScope<FeatureScreen>()
fun onDestroy() {
scope.close()
}
}
✅ DO: Use checkModules() in tests
@Test
fun checkKoinModules() {
koinApplication {
modules(allModules())
checkModules()
}
}
Platform-Specific Integration
Android App
You can keep using koin-android features and reuse the common modules/classes.
The code for the Android app can be found here: https://github.com/InsertKoinIO/hello-kmp/tree/main/androidApp
iOS App
The code for the iOS App can be found here: https://github.com/InsertKoinIO/hello-kmp/tree/main/iosApp
Calling Koin
Let’s prepare a wrapper to our Koin function (in our shared code):
// Helper.kt
fun initKoin(){
startKoin {
modules(appModule())
}
}
We can initialize it in our Main app entry:
@main
struct iOSApp: App {
// KMM - Koin Call
init() {
HelperKt.initKoin()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Injected Classes
Let’s call a Kotlin class instance from swift.
Our Kotlin component:
// Injection Boostrap Helper
class GreetingHelper : KoinComponent {
private val greeting : Greeting by inject()
fun greet() : String = greeting.greeting()
}
In our swift app:
struct ContentView: View {
// Create helper instance
let greet = GreetingHelper().greet()
var body: some View {
Text(greet)
}
}
Threading Considerations
On iOS and other Native targets, Koin instances are frozen by default with the new memory model. This generally works seamlessly, but be aware:
- Koin definitions are thread-safe
- Scopes can be created and used across threads
- Use
@SharedImmutablefor global Koin instances if needed
The new Kotlin/Native memory model (enabled by default in Kotlin 1.7.20+) makes Koin usage much simpler. For older projects, see the Kotlin Native Memory Management documentation.
Desktop Platform Integration
For JVM Desktop apps (Compose Desktop):
// Desktop main
fun main() = application {
// Initialize Koin
startKoin {
modules(appModules())
}
Window(onCloseRequest = ::exitApplication) {
App()
}
}
Desktop-specific modules:
// desktopMain
val desktopModule = module {
single<Logger> { DesktopLogger() }
single { DesktopFileManager() }
}
Web Platform Integration (Experimental)
For Kotlin/JS and Kotlin/WASM:
// jsMain or wasmJsMain
fun main() {
startKoin {
modules(appModules())
}
// Your web app initialization
}
val webModule = module {
single<Logger> { ConsoleLogger() }
single { BrowserStorage() }
}
WASM support is experimental. Some features may not work as expected.
Next Steps
- Android Integration: See Starting Koin on Android for Android-specific features
- Compose Multiplatform: See Koin Compose for Compose integration
- Annotations: See Koin Annotations KMP for annotation-based dependency injection
- Core Concepts: Review Modules, Scopes, and Definitions
- Testing: See Injecting in Tests for unit testing strategies