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
- Use Koin Annotations to simplify DI in Compose Multiplatform projects
- Gain compile-time safety and reduce runtime crashes
- Follow the step-by-step guide to try the working demo: https://github.com/kmpbits/KoinInjectDemo
🤔 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:
- Annotation-based DI
- Compile-time validation via KSP
- Cross-platform consistency
🧩 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:
- Your shared ViewModel annotated with
@KoinViewModel
- A global class to expose the dependency with
inject
- Koin started in the iOS app (usually in
iOSApp.swift
) - A SwiftUI-compatible wrapper to expose the ViewModel as an
ObservableObject
- It’s that simple. No extra wiring. No iOS-specific modules or hacks.
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:
- Cleaner DI setup
- Safer compile-time validation
- Less boilerplate, more fun
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
- GitHub: https://github.com/kmpbits/KoinInjectDemo
- Koin Docs: https://insert-koin.io/docs/
- KMP Bits: More tutorials and tools (coming soon)
🤝 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!