Skip to content
Go back

🧩 Injecting Fun: KMP + Koin Annotations Made Easy

by KMP Bits

KMP Bits Cover

Originally published on Medium

From Confusion to Clarity: Dependency Injection in Kotlin Multiplatform

If you’ve worked with Kotlin Multiplatform (KMP), you know setting up dependency injection (DI) across Android and iOS can be… let’s say, “fun.” Platform-specific quirks, mysterious runtime crashes — it’s a whole ride. Luckily, Koin now supports Koin Annotations for Compose Multiplatform, giving us a cleaner, safer, and modern DI experience — complete with compile-time checks.

In this article, I’ll walk you through the step-by-step setup I used in my KoinInjectDemo project, with tips, gotchas, and how you can get it running on both Android and iOS — without losing your mind.


TL;DR


🤔 Why This Matters

Dependency injection helps organize large codebases, promote testing, and reduce coupling. But in KMP, it often means juggling platform-specific configurations and runtime errors.

Koin Annotations bring:


🧩 Step-by-Step: Koin Annotations in Compose Multiplatform

Here’s exactly how I integrated Koin Annotations into my shared UI project.

🛠️ 1. Add Dependencies

In your libs.versions.toml:

koin = "3.5.3"
koinAnnotations = "1.3.0"
ksp = "1.9.23-1.0.20"

Then in composeApp/build.gradle.kts, apply KSP and add:

commonMain.dependencies {
    implementation(libs.koin.core)
    implementation(libs.koin.compose)
    implementation(libs.koin.composeVM)
    api(libs.koin.annotations)
}

dependencies {
    add("kspCommonMainMetadata", libs.koin.compiler)
    add("kspAndroid", libs.koin.compiler)
    add("kspIosSimulatorArm64", libs.koin.compiler)
    add("kspIosX64", libs.koin.compiler)
    add("kspIosArm64", libs.koin.compiler)
}

Why use api instead of implementation? Because the annotation-processed classes are shared and need to be visible to all targets.

🧪 2. Enable Configuration Check

Optional but helpful — in your ksp block:

ksp {
    arg("KOIN_CONFIG_CHECK", "true")
}

This enables build-time validation and helps you catch errors early.

🧬 3. Generate Metadata for Common Code

You need to configure Gradle to make the generated files available to commonMain:

tasks.withType<KotlinCompilationTask<*>>().configureEach {
    if (name != "kspCommonMainKotlinMetadata") {
        dependsOn("kspCommonMainKotlinMetadata")
    }
}

kotlin.sourceSets.getByName("commonMain") {
    kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
}

📦 4. Define a Manual Module Using @Module

Use this approach for third-party libraries or explicit control:

@Module
class GreetingModule {

@Single
    fun provideGreetingService(): GreetingService = GreetingService()
}

🔍 5. Use @ComponentScan for Auto-Wiring

Prefer automatic discovery? This lets Koin scan and bind annotated classes:

// Shared
@Module
expect class PlatformModule()

// Android
@Module
@ComponentScan("com.kmpbits.koininjectdemo.data.providers")
actual class PlatformModule

// iOS
@Module
@ComponentScan("com.kmpbits.koininjectdemo.data.providers")
actual class PlatformModule

This picks up @Single, @Factory, and @KoinViewModel classes automatically.

Tip: Add constructors to the expect/actual classes to ensure code generation works correctly.

🧠 6. Declare Your Application Class (Android)

In your AndroidManifest.xml:

<applicationandroid:name=".KoinInjectDemoApp"
    ... />

And in KoinInjectDemoApp.kt:

class KoinInjectDemoApp : Application() {
    override fun onCreate() {
        super.onCreate()
        initKoin()
    }
}

And in iOSApp.swift:

@main
struct iOSApp: App {

  init() {
       KoinComminKt.doInitKoin()
   }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .ignoresSafeArea()
        }
    }
}

💥 7. Gotchas & Compiler Help

Forget to bind something? The compiler will tell you — fast:

[ksp] --> Missing Definition for property 'platformLogProvider'

📱 Demo: What It Looks Like in Action

You can find the full working demo on GitHub.


🧠 Example: ViewModel with Dependencies

@KoinViewModel
class AppViewModel(
    private val greetingService: GreetingService,
    @Provided private val platformLogProvider: PlatformLogProvider
) : ViewModel() {

  private val _greetingState = MutableStateFlow("")
    val greetingState = _greetingState.asStateFlow()
    fun loadGreeting() {
        viewModelScope.launch {
            _greetingState.value = greetingService.greet()
        }
    }
    fun logMessage() {
        platformLogProvider.logMessage("Hello from AppViewModel!")
    }
}

Using @Provided is necessary here because we are mixing @ComponentScan for automatic bindings with manually declared dependencies, ensuring Koin knows which instances to inject without conflicts.

In your Compose UI:

val viewModel = koinViewModel<AppViewModel>()
val greeting by viewModel.greetingState.collectAsState()

Simple, reactive, and works cross-platform — no platform-specific wiring needed.


🧪 Does It Work With SwiftUI? Yes, It Does!

One of the most exciting things about Koin Annotations is that they’re not just useful for Android or Compose Multiplatform apps — they also work with SwiftUI on iOS when using Kotlin Multiplatform.

That means you can write your shared view models in Kotlin, annotate them with @KoinViewModel, and use them natively on the iOS side with SwiftUI. Dependency injection happens automatically, just like on Android, with no special setup.

⚙️ What You Need:

I might dive deeper into this in a dedicated article soon, but if you’re already using SwiftUI with KMP, give this a try — it’s smoother than you might expect.


📚 Summary

Koin Annotations bring:

If you’re using Compose Multiplatform, this setup makes DI simple and solid.


✅ Wrapping Up

Koin Annotations bring a fresh and simplified way to manage dependency injection in Kotlin Multiplatform — especially when you’re building with Compose Multiplatform. They reduce boilerplate, improve readability, and make your architecture cleaner without giving up flexibility.

Whether you’re building an Android app, a cross-platform UI with Compose, or even integrating with SwiftUI, this approach can scale from simple demos to serious production code. And the best part? You still get the power of Kotlin and KMP behind the scenes.

I’ve tested this setup with SwiftUI and it works surprisingly well — no hacks, no iOS-specific pain points. Just a clean way to share your logic across platforms.

If you’re starting your journey with Kotlin Multiplatform, or even just experimenting with modern DI tools, I hope this guide helped you get a real feel for what Koin Annotations can do. There’s still so much more to explore — like testing, error handling, and combining this with tools like Ktorfit or SQLDelight. Stay tuned for that!


🔗 Further Reading


🤝 Let’s Keep Going

This was a fun project — I hope it helps you kickstart DI in your KMP apps. If you try this setup or run into issues, let’s talk. I’d love to hear what you’re building!


Share this post on:

Previous Post
Flutter vs Kotlin Multiplatform: KMP with SwiftUI — Native Code with Kotlin Brain
Next Post
🚀 Good News for Kotlin Multiplatform Devs: It Just Got Way Easier!