Skip to main content
Kotlin Multiplatform Projects

The Definitive Guide to Kotlin Multiplatform Projects

Kotlin Multiplatform (KMP) promises shared business logic across Android, iOS, web, and desktop, but many teams struggle with integration, tooling, and common pitfalls. This guide cuts through the hype to provide a practical, problem-focused roadmap: from understanding how KMP actually compiles and shares code, to setting up a real project structure, navigating the ecosystem of libraries and build tools, and avoiding the mistakes that derail production adoption. We cover the trade-offs between KMP and traditional cross-platform approaches like Flutter and React Native, and offer concrete steps for incrementally introducing KMP into existing codebases. Whether you are evaluating KMP for a new project or trying to fix a struggling shared module, this guide gives you the decision criteria and hands-on advice you need to succeed.

Kotlin Multiplatform (KMP) promises shared business logic across Android, iOS, web, and desktop, but many teams struggle with integration, tooling, and common pitfalls. This guide cuts through the hype to provide a practical, problem-focused roadmap: from understanding how KMP actually compiles and shares code, to setting up a real project structure, navigating the ecosystem of libraries and build tools, and avoiding the mistakes that derail production adoption. We cover the trade-offs between KMP and traditional cross-platform approaches like Flutter and React Native, and offer concrete steps for incrementally introducing KMP into existing codebases. Whether you are evaluating KMP for a new project or trying to fix a struggling shared module, this guide gives you the decision criteria and hands-on advice you need to succeed.

Why KMP Still Feels Risky — and How to Make It Work

Every cross-platform technology promises code reuse, but the reality is often a tangle of platform-specific workarounds and fragile abstractions. Kotlin Multiplatform stands apart because it compiles to native binaries on each target—Android uses the Kotlin/JVM backend, iOS uses Kotlin/Native with LLVM, and web uses Kotlin/JS. This means your shared code runs directly on each platform without an interpreter or cross-compilation bridge. However, this architectural strength also introduces complexity: you must manage expect/actual declarations for platform APIs, configure Gradle modules for each target, and handle discrepancies in concurrency models (e.g., Kotlin/Native's strict memory model vs. JVM's garbage collection).

Teams often dive into KMP expecting a seamless 90% code-sharing ratio, only to discover that networking, storage, and UI patterns require careful design. The core pain is not the language itself but the lack of mature tooling for debugging, testing, and dependency management across platforms. For example, a typical project might share data models, business logic, and networking clients, but platform-specific UI code (SwiftUI vs. Jetpack Compose) remains separate. The value lies in reducing duplication in the middle tier—not eliminating platform code entirely.

We have seen teams succeed by starting small: sharing a single module for data models and API clients, then gradually expanding to repositories, use cases, and view models. The key is to define clear boundaries between shared and platform-specific code, and to invest in a robust build pipeline that runs tests on all targets before merging. This section sets the stage for understanding KMP's real trade-offs, so you can decide whether—and how—to adopt it.

The Real Cost of Shared Code

Shared code is not free. Maintaining expect/actual declarations for platform-specific APIs (like file I/O or sensors) adds overhead. A common mistake is to over-share code that is inherently platform-dependent, leading to complex abstractions that are harder to maintain than two separate implementations. We recommend profiling your app's codebase: identify the modules where business logic changes frequently and where platform-specific behavior is minimal—those are the best candidates for sharing.

How KMP Actually Works Under the Hood

Kotlin Multiplatform compiles your shared code into different outputs depending on the target. For Android, it produces JVM bytecode; for iOS, it generates a native framework (via Kotlin/Native); for JavaScript, it emits ES modules or CommonJS. The compiler front-end is the same, but the back-end and runtime differ. This means your code must be written in a subset of Kotlin that works across all targets—no reflection on iOS, no Java-specific APIs, and careful use of threading primitives.

The expect/actual mechanism is KMP's way of handling platform differences. You declare an expect function or class in common code, then provide actual implementations in each platform module. For example, you might expect a Platform object that returns the current device name, and provide actual implementations for Android and iOS. This pattern works well for small APIs but can become unwieldy for large interfaces. In practice, many teams wrap platform APIs behind a common interface and use dependency injection to provide platform-specific implementations at runtime.

Concurrency is another major difference. Kotlin/Native uses a strict memory model where objects are frozen by default and cannot be mutated from multiple threads without explicit @ThreadLocal or atomic wrappers. Kotlin/JVM, on the other hand, relies on the JVM's garbage collector and standard thread safety. This mismatch means that shared code must avoid mutable state or use safe concurrency patterns (e.g., coroutines with Dispatchers.Default). The Kotlin team is working on a unified memory model, but as of 2026, you still need to be aware of the differences.

Build Configuration Essentials

Setting up a KMP project requires a specific Gradle structure. The common module uses the org.jetbrains.kotlin.multiplatform plugin, and you define targets inside kotlin { } block. For each target, you can specify source sets (e.g., iosMain, androidMain) and dependencies. A typical pitfall is forgetting to configure the iOS framework output—you need to add binaries.framework { baseName = "shared" } inside the iOS target block. Without this, your iOS app won't be able to import the shared module.

Building a Real KMP Project: Step by Step

Let's walk through creating a KMP project from scratch, focusing on a practical scenario: a social media app with shared data models, a networking client, and a repository layer. We'll assume you have Android Studio and Xcode installed.

  1. Create the project structure: Use the KMP wizard in Android Studio (File > New > New Project > Kotlin Multiplatform App). This generates a project with shared, androidApp, and iosApp modules. Alternatively, you can start from a template on GitHub.
  2. Configure dependencies: In shared/build.gradle.kts, add the Ktor client for networking and kotlinx.serialization for JSON parsing. Use implementation for common dependencies and androidMainImplementation / iosMainImplementation for platform-specific ones.
  3. Define shared data models: Create data classes in the commonMain source set. Use @Serializable annotations for JSON serialization. Keep these models free of platform-specific code.
  4. Implement the networking client: Use Ktor's HttpClient with platform-specific engines: OkHttp on Android and Darwin on iOS. Define an expect class for the engine factory and provide actual implementations.
  5. Build the repository layer: Create repositories that use the networking client to fetch data and expose it as Flow or suspend functions. This layer can be fully shared.
  6. Expose shared code to iOS: Use the binaries.framework configuration to generate an iOS framework. Then, in Xcode, add the framework to your iOS app and import it in Swift code.
  7. Test on both platforms: Write unit tests in commonTest and run them on JVM and iOS simulators. Use @Test annotations from kotlin.test.

Common Pitfalls in the Build Phase

One frequent issue is the iOS framework not being regenerated after changes to shared code. Ensure your Gradle task embedAndSignAppleFrameworkForXcode runs before building the iOS app. Another pitfall is version conflicts between Kotlin and Ktor or kotlinx.serialization—always check the compatibility matrix on the Kotlin website.

Tools, Libraries, and Maintenance Realities

The KMP ecosystem has matured significantly, but it is still smaller than Android or iOS alone. For networking, Ktor is the standard choice, with support for HTTP/2, WebSockets, and serialization. For local storage, SQLDelight provides multiplatform SQLite support, while Multiplatform Settings offers a simple key-value store. For image loading, Coil has experimental KMP support. For dependency injection, Koin works across platforms, and Kotlin-inject is gaining traction.

However, not every library is available for all targets. For example, some Android-specific libraries (like Room) do not work on iOS. You may need to wrap platform APIs or use expect/actual patterns. The community maintains a curated list at KMP Libraries, but always verify the target support before committing.

Maintenance is another reality. KMP evolves rapidly—the Kotlin team releases new versions every few months, and breaking changes are common. You need a CI pipeline that tests all targets and a process for updating Kotlin versions and libraries simultaneously. Many teams adopt a cadence of updating KMP version once per quarter, after verifying compatibility with their dependencies.

Cost and Team Skills

KMP does not require licensing fees, but the learning curve for Kotlin/Native and Gradle configuration can be steep. You will need developers who are comfortable with Kotlin and willing to learn platform-specific tooling for iOS (Xcode, CocoaPods or SPM) and Android (Gradle, Android SDK). The total cost of ownership includes CI time for building on multiple platforms and potential delays due to tooling issues. For small teams, starting with a single shared module is often more manageable than a full rewrite.

Growing Your KMP Codebase: Incremental Adoption and Scaling

The most successful KMP projects do not start with a big bang rewrite. Instead, they identify a bounded module—like a networking layer or data repository—and extract it into a shared module while keeping the rest of the app platform-specific. This incremental approach reduces risk and allows the team to learn the tooling without blocking feature development.

As the shared module grows, you need to enforce architectural boundaries. Use a layered architecture: data models at the bottom, then repositories, then use cases or view models. Keep platform-specific code isolated in expect/actual declarations or dependency injection modules. Avoid putting UI logic in shared code—UI frameworks (Jetpack Compose, SwiftUI) are fundamentally different and should remain separate.

Another growth challenge is dependency management. With multiple targets, you may need different versions of the same library for different platforms. Use Gradle's commonMain dependencies for shared libraries and platform-specific source sets for target-specific ones. Consider using a version catalog (libs.versions.toml) to keep versions consistent.

Monitoring and Debugging

Debugging shared code on iOS is harder than on Android because you cannot use Android Studio's debugger for iOS. You can use Xcode's debugger with the generated framework, but it does not step through Kotlin source code—only the compiled binary. Use logging extensively and consider adding a shared logging interface that delegates to platform-specific loggers (Logcat on Android, os_log on iOS).

Risks and Pitfalls: What to Avoid

KMP has several well-known pitfalls that can derail a project. First, over-reliance on expect/actual for large APIs leads to maintenance burden. Instead, prefer wrapping platform APIs behind a common interface and using dependency injection. Second, ignoring Kotlin/Native's memory model can cause crashes on iOS. Always use kotlinx.coroutines with Dispatchers.Default for background work, and avoid mutable global state.

Third, not testing on all targets early. A common mistake is to write tests only on JVM, then discover that Kotlin/Native behaves differently (e.g., serialization of certain types). Set up CI to run tests on all targets from day one. Fourth, underestimating the build complexity. The Gradle configuration for KMP is verbose, and issues like missing framework embedding or incompatible Kotlin versions can take hours to debug.

Finally, beware of the "shared everything" fallacy. Not all code benefits from sharing. UI code, platform-specific hardware access, and code that depends on proprietary SDKs (like Google Maps or Apple Pay) should stay in platform modules. A good rule of thumb: share only code that would be identical if written twice.

Mitigation Strategies

To mitigate these risks, start with a small proof of concept that covers the full pipeline from shared code to running on both platforms. Document your expect/actual patterns in a style guide. Invest in a CI pipeline that builds and tests all targets, and use Gradle build caching to speed up builds. Consider using the Kotlin Wrappers for web targets if you need to share code with a web frontend.

Frequently Asked Questions About KMP

This section addresses common concerns we hear from teams evaluating or starting with KMP.

Can I share UI code with KMP?

KMP itself does not provide a cross-platform UI framework. However, you can use Jetpack Compose Multiplatform (currently in alpha) for sharing UI between Android and desktop, and experimental support for iOS. For production, most teams keep UI separate and share only business logic. If UI sharing is a priority, consider Flutter or React Native instead.

How does KMP compare to Flutter and React Native?

KMP compiles to native code, so it has no performance penalty from a virtual machine or bridge. Flutter uses its own rendering engine (Skia), which can give consistent UI but larger app size. React Native uses a JavaScript bridge, which can introduce latency. KMP is best for teams that already use Kotlin and want to share logic without changing their UI framework. Flutter is better for teams that want a single UI codebase. React Native is a middle ground with a large ecosystem.

Is KMP production-ready?

Yes, many companies (including Netflix, McDonald's, and VMWare) use KMP in production. However, the tooling is still evolving, and you may encounter bugs in Kotlin/Native or library incompatibilities. Plan for extra debugging time and keep your shared module small initially.

What about testing?

You can write unit tests in commonTest using kotlin.test. For integration tests, you need to run on each platform. Ktor provides a test engine that works on all targets. For UI testing, you still need platform-specific frameworks (Espresso, XCUITest).

Next Steps: From Evaluation to Production

If you are convinced that KMP fits your project, here is a concrete action plan:

  1. Set up a proof of concept: Create a new KMP project with a single shared module that contains data models and a simple networking client. Build and run on both Android and iOS.
  2. Identify your first shared module: Look at your existing codebase for a module that is self-contained, platform-agnostic, and frequently changed. A repository or API client is a good candidate.
  3. Extract incrementally: Move that module into the shared project, keeping the rest of the app untouched. Test thoroughly on both platforms.
  4. Establish team conventions: Document expect/actual patterns, dependency injection approach, and testing strategy. Use a style guide to ensure consistency.
  5. Invest in CI: Set up a pipeline that builds and tests all targets on every commit. Use Gradle build caching to keep build times manageable.
  6. Monitor and iterate: Track the percentage of shared code, build times, and bug rates. Adjust your strategy based on real data.

KMP is not a silver bullet, but for teams that need to share business logic across platforms without sacrificing native performance, it is a powerful tool. Start small, learn the nuances, and expand as your confidence grows. The key is to treat KMP as an architectural decision, not just a language feature.

About the Author

Prepared by the editorial contributors at languor.xyz, this guide is written for developers and team leads evaluating or adopting Kotlin Multiplatform. We have synthesized insights from community discussions, official documentation, and real-world project experiences to provide a balanced, practical perspective. While we strive for accuracy, the KMP ecosystem evolves rapidly; verify specific version compatibility against official Kotlin release notes and library documentation before making production decisions.

Last reviewed: June 2026

Share this article:

Comments (0)

No comments yet. Be the first to comment!