Beginner Fundamentals
What problem does Kotlin Multiplatform solve?
It lets teams write business logic — networking, data models, validation, caching — once in Kotlin and reuse it on Android, iOS, desktop, and web, instead of writing and maintaining separate implementations of the same logic per platform. Unlike Flutter or React Native, it doesn't require replacing native UI; you can keep fully native Android and iOS UI and share only the logic layer.
What is the difference between commonMain, androidMain, and iosMain?
commonMain contains code that compiles for every configured target. androidMain and iosMain contain platform-specific code that only compiles for that respective target — used for platform APIs that don't have a shared equivalent.
What does the `expect` keyword do, and what does `actual` do?
expect declares a function, class, or property signature in common code without providing an implementation. Each platform source set then provides a matching actual implementation. This is how shared code calls platform-specific functionality (like secure storage or device info) through a single common API.
Is Kotlin Multiplatform the same as Compose Multiplatform?
No. KMP is the underlying mechanism for sharing Kotlin code across platforms. Compose Multiplatform is built on top of KMP and specifically shares UI code using Compose APIs. You can use plain KMP (shared logic, native UI on each platform) without ever touching Compose Multiplatform.
What is Ktor Client used for in a KMP project?
Ktor Client is JetBrains' multiplatform HTTP client, used for making network requests from shared (commonMain) code so the same networking logic runs on both Android and iOS, rather than writing Retrofit calls on Android and URLSession calls on iOS separately.
Can you write unit tests that run on every KMP target?
Yes — tests placed in commonTest execute against every target the module is configured for (e.g. both the Android target and the iOS/Kotlin-Native target), so one test suite verifies the same shared logic works correctly everywhere.
Intermediate Architecture & Tooling
Why is Koin more commonly used than Hilt in KMP projects?
Hilt is built on Dagger and tightly coupled to Android's annotation-processing build tooling, so it can't run in commonMain code shared with iOS. Koin is a lightweight, reflection-based DI framework with no platform-specific build dependency, so it works identically across every KMP target.
Why is SQLDelight often preferred over Room for KMP local persistence?
Room only targets Android. SQLDelight generates type-safe Kotlin query APIs from plain SQL (.sq) files and compiles against a native SQLite driver on each platform, giving you one shared persistence layer and one set of queries instead of maintaining Room on Android and Core Data on iOS separately.
How do suspend functions in shared Kotlin code get called from Swift?
The Kotlin/Native compiler translates a Kotlin suspend function into a Swift function that takes a completion-handler closure, since Objective-C/older Swift interop has no native concept of Kotlin coroutines. Newer Kotlin/Swift interop tooling can also bridge suspend functions more directly to Swift's async/await in supported configurations.
What is a common strategy for incrementally adopting KMP in an existing native app?
Start by extracting a small, self-contained piece of logic that's duplicated on both platforms — commonly networking/API calls or data validation — into a shared module, wire it into both existing native apps, and only expand scope once that's proven out. A full-app rewrite in one step is high-risk and not how most production teams adopt KMP.
What Kotlin language features translate imperfectly into Swift, and why does it matter for API design?
Default parameter values, sealed classes/interfaces, and inline value classes don't map one-to-one onto Objective-C/Swift interop and can produce awkward or verbose generated Swift APIs. Because of this, teams designing a shared module's public API for iOS consumption often add explicit overloads or wrapper functions instead of relying on Kotlin-only conveniences at the interop boundary.
What's the difference between sharing logic-only vs. sharing UI with Compose Multiplatform, from a product perspective?
Logic-only sharing keeps 100% native UI on each platform (native look, feel, and platform-specific UX conventions) at the cost of writing UI twice. Compose Multiplatform shares UI code too, cutting UI development time significantly, but the iOS app's look and feel is driven by Compose rather than fully native SwiftUI conventions — a tradeoff each team evaluates based on how much native platform fidelity matters for their product.
Advanced Deep Dives
How does Kotlin/Native handle memory management differences with Swift/Objective-C's ARC?
Kotlin/Native has its own memory manager (the modern, default one uses a tracing garbage collector), while Swift/Objective-C use Automatic Reference Counting (ARC). At the interop boundary, Kotlin objects passed to Swift are wrapped so ARC can manage their lifetime correctly, and the Kotlin/Native runtime coordinates with ARC to avoid premature deallocation or leaks — this used to require more manual care in older Kotlin/Native memory models than it does with the current default GC-based manager.
How would you structure a shared module's public API to keep Swift interop clean at scale?
Keep the shared module's public surface intentionally small and stable — expose repository/use-case-style classes and simple data classes rather than deep Kotlin-idiomatic hierarchies, avoid default arguments and inline classes on public APIs, and use sealed classes sparingly (pattern-matched via onEnum(of:) helpers on the Swift side) since large sealed hierarchies generate verbose Objective-C headers.
What are the tradeoffs of targeting iosX64, iosArm64, and iosSimulatorArm64 all in one shared module?
Each target produces a separate compiled klib/framework slice — iosArm64 for physical devices, iosSimulatorArm64 for Apple Silicon Macs' simulator, and iosX64 for older Intel-Mac simulators. Supporting all three increases build time and CI matrix complexity but ensures the shared framework runs correctly regardless of which Mac architecture or device the iOS team builds against; many teams now drop iosX64 since Apple Silicon Macs are standard.
How do you handle a platform API that has no expect/actual equivalent and needs deep platform-specific behavior (e.g. background work scheduling)?
Rather than forcing a shared abstraction over fundamentally different platform mechanisms (WorkManager on Android vs. BGTaskScheduler on iOS), a common pattern is to keep the scheduling trigger itself platform-native, but have each platform's scheduled task call into a shared commonMain function that does the actual work — sharing the logic that matters while accepting that the "when do I run" mechanism stays platform-specific and un-abstracted.