Cross-platform development has long promised write-once-run-anywhere efficiency, but many teams have found that promise hollow when faced with platform-specific quirks and maintenance burdens. Kotlin Multiplatform (KMP) offers a different approach: share business logic while keeping native UIs, or go all-in with Compose Multiplatform for shared UI. This guide distills practical insights from teams that have navigated KMP adoption, focusing on what works, what doesn't, and how to decide if KMP is right for your next project. We cover core concepts, step-by-step workflows, tooling comparisons, and common pitfalls—all grounded in real-world scenarios, not theoretical ideals.
Why Kotlin Multiplatform? Understanding the Stakes and Reader Context
Many organizations face a painful choice: maintain separate codebases for Android and iOS (and sometimes web and desktop), or adopt a cross-platform framework that may limit native capabilities or introduce abstraction overhead. KMP aims to reduce duplication by allowing you to write shared code in Kotlin, which compiles to platform-specific binaries. The key value proposition is not necessarily 100% code sharing—rather, it's about sharing the parts that benefit most from reuse: data models, business rules, networking, and persistence.
The Real Cost of Duplication
In a typical project, a team might spend 30-40% of development time on business logic that is identical across platforms. When bugs appear, they must be fixed in two (or more) places, doubling effort. KMP addresses this by centralizing that logic in a shared module. However, the trade-off is that you must learn Kotlin Multiplatform's expect/actual mechanism, which allows you to declare platform-agnostic APIs and provide platform-specific implementations. Teams often underestimate this learning curve, especially when integrating with platform SDKs like Android's Jetpack or iOS's UIKit.
When KMP Makes Sense
KMP shines in projects where the business logic is complex and stable, and where platforms need to feel native. For example, a fintech app with intricate transaction rules and compliance checks benefits from shared logic, while its UI remains tailored to each platform's design guidelines. Conversely, a simple content-consumption app with minimal shared logic may not justify the overhead. Teams should also consider their existing Kotlin expertise—if your Android team already uses Kotlin, the learning curve is significantly lower than if you're coming from a pure Swift or JavaScript background.
Another critical factor is the maturity of the ecosystem. As of May 2026, KMP is stable for production use in Android and iOS, with growing support for web (via Kotlin/JS) and desktop (via Kotlin/Native). However, libraries for specific tasks—like camera access or Bluetooth—may still require platform-specific code. The decision to adopt KMP should be based on a honest assessment of your team's skills, the app's requirements, and the willingness to invest in tooling and build configuration.
Core Concepts: How Kotlin Multiplatform Works
At its heart, KMP uses a source set hierarchy. You define shared code in a commonMain source set, which is compiled for all targets. Platform-specific code lives in androidMain, iosMain, etc. The expect/actual mechanism lets you declare a function or class in common code (using the expect keyword) and provide platform-specific implementations (using actual). This is how you interact with platform APIs while keeping the interface shared.
Understanding the Expect/Actual Mechanism
Suppose you need to get the current device timezone. In commonMain, you write: expect fun getDeviceTimezone(): TimeZone. Then in androidMain, you implement it using Android's java.util.TimeZone, and in iosMain, you use NSTimeZone. The compiler ensures that each platform provides the actual implementation. This mechanism works well for simple APIs, but can become cumbersome for complex interfaces with many methods. In such cases, consider using a dependency injection framework or a platform-agnostic library like Ktor for networking, which already provides multiplatform implementations.
Shared Module Architecture
A typical KMP project structure includes a shared module containing common code, and separate app modules for each platform. The shared module can be a library that exports its API to the app modules. Inside the shared module, you organize code into packages like data (repositories, data sources), domain (use cases, models), and platform (expect declarations). This separation mirrors clean architecture and makes testing easier. For instance, you can write unit tests for domain logic in common code without any platform dependencies.
One common mistake is trying to push too much code into the shared module. UI code, for example, is often better kept platform-specific unless you use Compose Multiplatform. Similarly, direct access to platform sensors or file systems should be abstracted via expect/actual. The rule of thumb: share the logic that would be identical if written twice, and keep platform-specific code that would differ anyway.
Execution: Step-by-Step Workflow for Setting Up a KMP Project
Setting up a KMP project from scratch involves several steps, from choosing the right build system to configuring dependencies. Below is a practical workflow that many teams follow.
Step 1: Choose Your Targets and Build System
KMP officially supports Gradle as its build system. Start by creating a new project using the Kotlin Multiplatform wizard (available at kotlinlang.org) or manually setting up a Gradle multi-module project. Define your targets in the shared module's build.gradle.kts. For example, to target Android and iOS, you add androidTarget() and iosX64(), iosArm64(), iosSimulatorArm64(). For iOS, you also need to configure the framework output (e.g., binaries.framework { baseName = "shared" }).
Step 2: Configure Dependencies
Use the commonMain dependencies block to add multiplatform libraries. For example, Ktor for HTTP, SQLDelight for local storage, and kotlinx.serialization for JSON parsing. Platform-specific dependencies (like Android's OkHttp or iOS's Foundation) go in their respective source sets. Be cautious with version conflicts—use a Bill of Materials (BOM) if available. Many teams find it useful to create a buildSrc or version catalog to centralize dependency versions.
Step 3: Implement Shared Logic
Start with the domain layer: define your data models (using @Serializable data classes), repository interfaces, and use cases. Then implement the data layer: create platform-agnostic implementations using Ktor for API calls and SQLDelight for local caching. For platform-specific behavior (like file storage), use expect/actual. For example, expect a PlatformContext class that provides the app's file directory, and implement it on Android using Context.filesDir and on iOS using NSFileManager.
Step 4: Integrate with Platform Apps
In your Android app module, add a dependency on the shared module. You can then call shared functions directly from your Activities or ViewModels. For iOS, you'll need to create an Xcode project that imports the shared framework. Use a script phase in Xcode to build the Kotlin framework automatically. Many teams use the embedAndSignAppleFrameworkForXcode Gradle task to streamline this. Once integrated, you call shared functions from Swift as if they were native classes.
Step 5: Test and Iterate
Write unit tests for common code using kotlin.test and run them on JVM (fast) or native targets (for platform-specific behavior). For integration tests, you may need to run on actual devices or simulators. Use continuous integration (CI) pipelines that build both Android and iOS targets. Common CI tools like GitHub Actions or Jenkins can be configured with Gradle tasks to build the shared module and run tests.
Tools, Stack, and Economics: Choosing the Right Libraries and Managing Costs
Selecting the right libraries is crucial for KMP project success. The ecosystem has matured, but not every library is multiplatform-ready. Below is a comparison of commonly used libraries across key categories.
| Category | Library | Multiplatform Support | Pros | Cons |
|---|---|---|---|---|
| Networking | Ktor | Full (Android, iOS, JVM, JS, Native) | Lightweight, coroutine-based, easy to test | Smaller community than Retrofit; less plugin ecosystem |
| Networking | OkHttp (via Ktor engine) | Android only (directly) | Mature, widely used | Not multiplatform; must use Ktor engine for iOS |
| Local Storage | SQLDelight | Full (Android, iOS, JVM, JS, Native) | Type-safe SQL, compile-time verification, multiplatform | Learning curve for SQL generation; not NoSQL |
| Local Storage | Room | Android only | Familiar to Android devs, integrates with Jetpack | Not multiplatform; requires platform-specific code |
| Serialization | kotlinx.serialization | Full | First-party, compile-time, supports JSON, ProtoBuf, CBOR | Requires compiler plugin; less flexible than Gson for complex cases |
| UI | Compose Multiplatform | Android, iOS, Desktop, Web (experimental) | Shared UI, declarative, modern | iOS support still maturing; performance overhead on iOS |
| UI | Native UI (SwiftUI + Jetpack Compose) | Platform-specific | Best performance, full platform features | No code sharing for UI; more code to maintain |
Economic Considerations
Adopting KMP can reduce development costs by eliminating duplicate business logic, but it introduces new costs: training, build configuration, and potential debugging complexity. Teams should budget for a pilot project—perhaps a small feature or a new app—to evaluate the learning curve and tooling maturity. Many practitioners report that the break-even point occurs after the first two platform releases, as shared code stabilizes and platform-specific code becomes thinner. However, if your app requires heavy use of platform-specific features (AR, Bluetooth, camera), the shared percentage may be low, making KMP less economical.
Growth Mechanics: Scaling KMP Projects and Maintaining Momentum
Once a KMP project is up and running, the focus shifts to scaling the codebase and team. Growth in this context means adding more features, more platforms, and more developers—all while keeping the shared codebase healthy.
Adding New Platforms
KMP's architecture makes adding a new platform (e.g., desktop or web) relatively straightforward. You create a new source set for that platform and provide actual implementations for any expect declarations. However, you may need to add new expect declarations if the new platform lacks certain APIs. For example, adding desktop support may require expect declarations for file dialogs or system tray. Plan for this by keeping expect declarations minimal and generic. Also, consider using Compose Multiplatform for UI if you want to share UI across desktop and mobile, but be aware that desktop UI patterns differ from mobile.
Onboarding New Developers
New team members often struggle with the expect/actual pattern and the Gradle build configuration. Create a developer onboarding guide that includes a sample project, a list of common pitfalls, and a glossary of KMP terms. Pair programming during the first week can help. Also, enforce code reviews that specifically check for proper use of platform-specific code—developers sometimes accidentally put platform-specific code in commonMain, which will not compile for other targets.
Maintaining Build Performance
As the shared module grows, build times can increase, especially for iOS targets because Kotlin/Native compilation is slower than JVM. Mitigate this by using Gradle build caching, incremental compilation, and by splitting the shared module into smaller submodules if necessary. For example, separate networking logic from business logic into different modules so that changes to one don't trigger recompilation of the other. Some teams also use remote build caches to share compiled artifacts across developers.
Continuous Integration and Delivery
Set up CI pipelines that build and test all targets. For iOS, you'll need a macOS runner. Use Gradle tasks like linkDebugFrameworkIosArm64 to produce the framework, and then run Xcode tests. Automate versioning and publishing of the shared framework to a private repository (e.g., via GitHub Packages or Artifactory) so that iOS developers can consume it without rebuilding from source. This decouples the shared module's release cycle from the app's release cycle.
Risks, Pitfalls, and Mitigations
No technology is without risks. KMP has several known pitfalls that can derail a project if not addressed early.
Dependency Management Conflicts
Because KMP compiles to multiple platforms, dependencies must be available for each target. A library that works on Android may not have an iOS artifact. Always check the library's documentation for multiplatform support. Use the expect keyword to abstract away dependencies that are not multiplatform. For example, if you need a DI framework, Koin has multiplatform support, while Dagger does not. Mitigation: create a dependency compatibility matrix before starting the project, and prefer first-party Kotlin multiplatform libraries.
UI Integration Challenges
If you choose to keep native UIs, you must bridge data between the shared module and the UI layer. On Android, this is straightforward via ViewModels that observe shared flows. On iOS, you need to ensure that the shared framework's objects are accessible from Swift. Use @ObjCName annotations to rename Kotlin functions for Swift, and consider using a wrapper layer that converts Kotlin types to Swift types. A common pitfall is that Kotlin's coroutine Flow does not map directly to Swift's Combine framework; you may need to use a bridge library or convert flows to callback-based APIs.
Build Configuration Complexity
Setting up the Gradle build for multiple targets can be error-prone. Common issues include incorrect target names, missing platform-specific dependencies, and misconfigured framework linking. Mitigation: use the Kotlin Multiplatform wizard to generate the initial project, and keep the build files as simple as possible. Avoid custom Gradle plugins unless necessary. Document the build setup in a README so that new team members can reproduce it.
Testing Platform-Specific Code
Testing actual implementations on iOS requires running tests on a Mac with an iOS simulator. This adds CI complexity and cost. One mitigation is to keep actual implementations as thin as possible—ideally, they just delegate to a platform API. Then you can test the shared logic thoroughly in common tests, and rely on manual or integration tests for the thin platform layer. Also, consider using mocking frameworks like MockK that support multiplatform.
Performance Overhead on iOS
Kotlin/Native generates native binaries, but there can be overhead from memory management (Kotlin uses its own allocator) and from interop with Objective-C. For most apps, this overhead is negligible, but for performance-critical code (e.g., real-time audio processing), you may need to write platform-specific implementations. Profile early and often. If you find a bottleneck, consider moving that code to a platform-specific actual implementation.
Decision Checklist and Mini-FAQ
Before committing to KMP, run through this checklist to assess fit:
- Team skills: Does your team already know Kotlin? If not, factor in training time (at least 2-4 weeks for experienced developers).
- Shared logic percentage: Estimate how much code will be shared. If less than 30%, KMP may not be worth the overhead.
- Platform-specific features: Does your app rely heavily on platform SDKs (camera, Bluetooth, sensors)? If yes, plan for more actual implementations.
- UI approach: Are you willing to use Compose Multiplatform or maintain separate UIs? Both have trade-offs.
- Build infrastructure: Do you have macOS CI runners for iOS builds? If not, factor in cost.
- Library availability: Are the libraries you need available for all target platforms? Check before starting.
- Long-term maintenance: Is your organization committed to maintaining KMP-specific build configurations and expect/actual declarations?
Frequently Asked Questions
Q: Can I use KMP with an existing Android app? Yes, you can add a shared module incrementally. Start by moving data models and networking code to the shared module, then gradually shift business logic. Your existing Android app can depend on the shared module without affecting the iOS app until you're ready.
Q: How does KMP compare to Flutter or React Native? KMP is more flexible in terms of UI—you can choose to share UI or not—while Flutter and React Native enforce their own UI frameworks. KMP also has better performance for computation-heavy tasks because it compiles to native code. However, Flutter has a larger ecosystem for UI components and hot reload. The choice depends on your team's skills and UI requirements.
Q: Is KMP production-ready for iOS? Yes, as of May 2026, KMP is stable for iOS. Major companies like Netflix and McDonald's have used KMP in production. However, you may encounter edge cases with interop or performance; the community is active and responsive.
Q: What are the best resources to learn KMP? The official Kotlin documentation is excellent. Additionally, the Kotlin Slack community and the #multiplatform channel are very helpful. There are also several books and online courses, but always verify that they cover the latest version (1.9.x or later).
Synthesis and Next Actions
Kotlin Multiplatform is a powerful tool for sharing business logic across platforms, but it is not a silver bullet. Success requires careful planning, a willingness to invest in build configuration, and a clear understanding of when to share and when to keep code platform-specific. Start small: pick a single feature or a new app, build a shared module with a few expect/actual declarations, and measure the impact on development speed and code quality.
Next steps for teams considering KMP:
- Evaluate your current codebase: Identify the parts that are duplicated across platforms and estimate the effort to extract them into a shared module.
- Set up a prototype: Follow the step-by-step workflow above to create a minimal shared module that compiles for Android and iOS. Integrate it into a simple app (e.g., a weather app with a network call).
- Test the developer experience: Have one developer from each platform work on the prototype for a week. Note pain points and areas where the tooling falls short.
- Decide on UI strategy: Choose between Compose Multiplatform or native UIs based on your team's skills and design requirements. If in doubt, start with native UIs to minimize risk.
- Plan for CI: Set up a CI pipeline that builds and tests all targets. This is non-negotiable for a production project.
- Monitor and iterate: After launch, track metrics like crash rates, build times, and developer satisfaction. Adjust your architecture as needed.
Remember that KMP is a means to an end—delivering value to users faster and with fewer bugs. Keep that goal in mind, and you'll navigate the trade-offs effectively.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!