Back to portfolio

Guide · Updated 2026-06-08

Kotlin Multiplatform in Production

A practical guide to shipping Kotlin Multiplatform in production. Covers architecture, performance, CI/CD, iOS interop, and real-world KMP challenges.

Is Kotlin Multiplatform production ready?

Short answer: yes. KMP went stable in November 2023 and is now powering production apps at McDonald's, Netflix, Philips, Forbes, 9GAG, Cash App, Baidu, and Wattpad. The compiler, tooling (Kotlin 2.x with the K2 compiler), and ecosystem libraries — Ktor, SQLDelight, Koin, kotlinx.serialization, kotlinx.coroutines — are mature enough to ship to millions of users.

What is not fully stable yet: Compose Multiplatform for iOS is in Beta. Sharing business logic with native UI on each platform is the conservative, battle-tested path. Compose for iOS is viable for internal tools and greenfield products, but most production teams still ship SwiftUI on iOS today.

How does Kotlin Multiplatform work?

KMP is a compiler technology, not a runtime. Your shared Kotlin code is compiled to platform-specific artifacts:

  • Android — JVM bytecode, packaged as an AAR.
  • iOS — native ARM64/x86_64 binaries via Kotlin/Native, packaged as an Objective-C-compatible XCFramework (or static framework) that Swift consumes directly.
  • Desktop / Server — JVM.
  • Web — JavaScript or WebAssembly.

You organize code into commonMain (shared), androidMain, iosMain, etc. Platform contracts are declared with expect/actual:

// commonMain
expect class PlatformLogger() {
    fun log(message: String)
}

// androidMain
actual class PlatformLogger {
    actual fun log(message: String) = Log.d("App", message)
}

// iosMain
actual class PlatformLogger {
    actual fun log(message: String) = NSLog("App: %@", message)
}

Production architecture that actually scales

The pattern I've shipped most successfully across teams looks like this:

shared/
  commonMain/
    domain/        # entities, use cases (pure Kotlin)
    data/          # repositories, DTOs, mappers
    network/       # Ktor client, interceptors
    persistence/   # SQLDelight schemas + queries
    presentation/  # ViewModels / StateFlow (KMP-ViewModel)
  androidMain/     # actuals: DataStore, WorkManager glue
  iosMain/         # actuals: Keychain, BGTasks glue

androidApp/        # Jetpack Compose UI
iosApp/            # SwiftUI UI, consumes shared as XCFramework

Keep commonMain pure Kotlin. Push every platform-specific dependency (file system, push tokens, biometrics, deep links) behind an expect interface. ViewModels expose StateFlow; SwiftUI subscribes via a thin SKIE or hand-written observable wrapper.

Performance: what to measure, what to fix

  • iOS binary size. Kotlin/Native frameworks are large. Enable -Xbinary=bundleId=..., strip debug symbols for release, and split the framework if you ship multiple feature modules.
  • Startup on iOS. Class initialization is eager. Avoid heavy top-level vals in commonMain; lazy-initialize Koin modules.
  • Memory model. The new memory manager (default since Kotlin 1.7.20) removes the old freezing rules — but flows crossing into Swift still need explicit Dispatchers.Main on iOS.
  • Baseline Profiles on Android. Generate them against the shared module too; AOT-compiling shared code gives measurable cold-start wins.

CI/CD without surprises

  • Build the iOS XCFramework on macOS runners — Linux can't link Kotlin/Native iOS targets.
  • Publish the framework to a private CocoaPods spec repo, Swift Package Registry, or directly via SPM with a binary target.
  • Version the framework with the app, not independently — Swift consumers don't tolerate ABI drift well.
  • Cache ~/.konan aggressively; cold Kotlin/Native builds are slow.

Real-world challenges (and honest answers)

  • iOS team buy-in. The biggest blocker is rarely technical. Pair an iOS engineer on the shared module from day one.
  • Debugging into Kotlin from Xcode. Improving, but still rougher than native. Keep error messages descriptive.
  • Swift interop edge cases. Sealed classes, default arguments, and inline classes don't translate cleanly. SKIE from Touchlab closes most of the gap.
  • Library churn. Pin versions, watch the Kotlin release schedule, and budget time per release for tooling upgrades.

When to adopt KMP — and when not to

Adopt KMP when you have two native teams duplicating business logic, your domain is non-trivial, and you can justify the upfront tooling investment over 6+ months.

Don't adopt KMP when the app is mostly UI with a thin client, your team has zero Kotlin experience, or you need a single codebase including UI on a tight deadline (Flutter or React Native are a better fit there).

Further reading