← All posts

KMM in production — lessons learned after 6 months

April 15, 2026 6 min read

KMM sold me on the pitch: write your business logic once, share it across Android and iOS. After six months working with it in a production context, I have a more complicated answer.

The short version: KMM is real, delivers on specific things, and is still rough in ways that matter for team productivity. Here’s what I actually found.

What KMM actually is

KMM is not cross-platform UI. It’s cross-platform logic.

You write a shared Kotlin module — repositories, use cases, data models, networking — and consume it from both Android (as a Gradle dependency) and iOS (as an Objective-C framework). Each platform builds its own native UI. No shared rendering layer, no compromises on native look and feel.

       Android app                       iOS app
  +------------------+            +------------------+
  | Compose / XML UI |            | SwiftUI / UIKit  |
  | ViewModels       |            | ViewModels       |
  +------------------+            +------------------+
           |                               |
           | Gradle dependency             | ObjC framework
           v                               v
  +----------------------------------------------------+
  |               shared Kotlin module                 |
  |   use cases . repositories . data models           |
  |   Ktor + kotlinx.serialization . SQLDelight        |
  +----------------------------------------------------+

  shared: logic and data        own: everything drawn

This is the right trade-off on paper. Business logic is genuinely shareable. UI requirements diverge by platform. Share what you can share, build what needs to be native.

What actually worked

Networking and data models. Ktor with Kotlinx Serialization on shared code works well. You write the API layer once — request and response models, error handling, authentication headers. Android and iOS consume the same data from the same source. Discrepancies in how the two platforms parse a response become impossible by definition, which eliminates an entire category of subtle cross-platform bugs.

The repository layer. A shared repository that owns caching logic worked without major friction. SQLDelight integrates cleanly with KMM and generates Kotlin (Android) and Swift (iOS) from the same schema. One migration, one source of truth, two consumers.

Simple domain logic. Use cases that filter lists, compute derived values, and apply business rules shared cleanly. Both platforms get identical behavior with zero duplication. For a feature with complex validation rules, this is genuinely valuable — you change the logic once and both platforms are updated.

What was harder than expected

Almost every rough edge sits on one edge of the diagram above — the path from Kotlin to Swift.

  shared Kotlin --> ObjC framework --> Swift call site
        |                  |                 |
        |                  |                 +-- coroutines need
        |                  |                     SKIE to bridge
        |                  +-- regenerated per build (slow sync)
        |
        +-- breakpoints reachable only through LLDB
        +-- unreadable to an iOS team that doesn't write Kotlin

  the Android edge is just a Gradle dependency — no friction

IDE support. Kotlin Multiplatform support in Android Studio has improved over the past two years, but expect phases where iOS framework generation is slow, error highlighting in the shared module is wrong, or Gradle sync behaves unexpectedly. I lost real hours to tooling during the first two months. It gets better, but budget for it.

Debugging across the boundary. When something breaks in the shared code, the debugging experience is less smooth than debugging pure Kotlin or pure Swift. Setting breakpoints in shared code from the iOS side requires LLDB and isn’t as seamless as debugging within Xcode. Most issues are eventually findable. The feedback loop is slower than you’d like during active development.

Coroutines on iOS. Kotlin coroutines don’t map directly to Swift’s concurrency model. Libraries like SKIE bridge the gap well, but this adds a dependency that requires maintenance as both Kotlin and Swift evolve independently. If your shared code is coroutine-heavy — and any real-world networking code is — iOS consumption has friction that Android doesn’t.

Team capability split. If your iOS team isn’t comfortable reading Kotlin, the shared module becomes a black box to them. They can consume it but can’t debug or contribute to it. This creates bottlenecks and occasionally results in Android assumptions baked into shared code without iOS review. The organizational alignment required for KMM is just as important as the technical integration.

KMM vs Flutter Add-to-App

I’ve worked with both. They solve different problems and the choice matters.

KMM shares logic between fully native apps. If both platforms are mature native codebases with established teams, KMM lets you unify the business logic layer without displacing either platform team. The UI stays native. The investment in native expertise is preserved.

Flutter Add-to-App embeds Flutter screens into a native host. You get full cross-platform UI for new features, with the native app handling routing and platform-specific integrations. The trade-off: you’re now maintaining a Flutter codebase alongside your native code, and the Flutter team needs different skills than your Android or iOS engineers.

For a greenfield product: Flutter. Faster iteration, one codebase for UI and logic, one team to hire for.

For a mature dual-native codebase where both platform teams are invested and capable: KMM. It fits into what exists rather than displacing it.

What I’d do differently

Start with a narrow scope. Pick one module — an authentication flow, a data sync layer, a configuration service — and ship it in production before expanding. Teams that try to share everything from day one run into integration problems across the codebase simultaneously, and each one is harder to diagnose because the surface area is so large.

Align both platform teams before starting. iOS engineers need to be comfortable consuming Kotlin code and contributing to the shared module. That’s an organizational commitment before it’s a technical one. If the iOS team isn’t bought in, KMM becomes an Android team project that iOS incidentally consumes — and the architecture reflects that.

The honest verdict

KMM is production-ready for what it’s designed to do: shared business logic in a dual-native codebase. It’s not yet as frictionless as building purely for one platform. The tooling has rough edges. The iOS story has complexity.

If you have two mature native apps duplicating business logic across Android and iOS: KMM is worth the investment. If you’re evaluating cross-platform from scratch: decide first whether you want shared logic or shared UI. The answer to that question determines which tool fits. Picking the tool before answering the question is how teams end up with the wrong architecture for their situation.