Cross-platform development has long promised write-once-run-anywhere efficiency, but practical implementations often fall short. Kotlin Multiplatform (KMP) takes a different approach: share business logic while keeping native UIs. This guide provides a strategic framework for evaluating and adopting KMP, based on widely shared professional practices as of May 2026. We focus on real-world trade-offs, common mistakes, and a repeatable workflow to help you decide if KMP is right for your team.
Why Cross-Platform Still Feels Like a Compromise
Many teams adopt cross-platform frameworks to reduce duplication, but they often encounter new problems: performance bottlenecks, limited access to platform APIs, or a user interface that feels out of place. The core tension is between code sharing and platform fidelity. KMP addresses this by keeping the UI layer native—iOS uses SwiftUI, Android uses Jetpack Compose—while sharing only the non-UI logic: networking, data validation, business rules, and state management.
The Shared-Layer Fallacy
A common misconception is that KMP can share the entire app. In practice, the shared layer is typically 30–60% of the codebase, depending on how much platform-specific behavior the app requires. Teams that expect 80%+ sharing are often disappointed. The real efficiency gain comes from reducing duplication in the model, repository, and network layers, not from sharing UI code.
Another overlooked factor is team expertise. KMP requires Kotlin proficiency on both iOS and Android sides. iOS developers need to learn Kotlin (or at least read it), and Android developers must understand Kotlin/Native memory management. This learning curve can offset early productivity gains. One team I read about spent the first two months of a project just setting up the shared module and CI pipeline—time they had not budgeted for.
Despite these challenges, KMP offers a unique advantage: it avoids the UI abstraction layer that frameworks like React Native or Flutter impose. By keeping the UI native, KMP apps feel more platform-consistent, and developers can use the latest platform APIs without waiting for a framework to catch up. This makes KMP particularly attractive for apps that demand high UI fidelity or heavy use of platform features like camera, Bluetooth, or ARKit.
How Kotlin Multiplatform Works Under the Hood
KMP compiles Kotlin code to different targets: JVM bytecode for Android, native binaries for iOS (via Kotlin/Native), and JavaScript for web. The shared module uses a set of Kotlin APIs that are available across all targets, with platform-specific implementations provided through expect/actual declarations. This mechanism lets you declare a function or class in the common module and provide platform-specific implementations in each target's source set.
Expect/Actual Declarations
For example, you might declare an expect fun getPlatformName(): String in the common module, then implement it as actual fun getPlatformName(): String = "Android" in the Android source set and actual fun getPlatformName(): String = "iOS" in the iOS source set. This pattern keeps the common code clean while allowing platform-specific behavior where needed. However, overusing expect/actual can fragment the codebase; the goal is to minimize platform-specific code.
Memory Management Differences
On Android, KMP uses the JVM's garbage collector. On iOS, Kotlin/Native uses automatic reference counting (ARC) similar to Swift. This difference can cause subtle memory issues: objects that are garbage-collected on Android may be retained on iOS if reference cycles are not handled. Teams must be mindful of weak references and lifecycle management, especially when passing shared objects to platform code.
Another key detail is that Kotlin/Native does not support reflection fully, which means some libraries (like those relying on Java reflection) may not work on iOS. This can affect serialization frameworks, dependency injection containers, and ORM tools. KMP-compatible libraries (e.g., kotlinx.serialization, Ktor) are designed to work around this, but teams migrating existing libraries may face limitations.
A Repeatable Workflow for Adopting KMP
Adopting KMP is not a single decision but a series of incremental steps. The following workflow has been refined by multiple teams and can be adapted to your project's constraints.
Step 1: Audit Your Codebase
Identify which modules are primarily business logic and which are UI or platform-dependent. Use a dependency graph to see how tightly coupled your layers are. The best candidates for sharing are modules with minimal platform dependencies: network clients, data models, validators, and state management (e.g., using a unidirectional data flow architecture). Avoid sharing modules that rely heavily on platform APIs like location, camera, or biometrics unless you are willing to maintain expect/actual wrappers.
Step 2: Start with a Single Shared Module
Do not try to share everything at once. Create a single KMP module for the networking layer (e.g., using Ktor) and the data models. Integrate it into both the Android and iOS apps, running the existing test suite to ensure behavior matches. This often reveals issues with serialization, threading, or missing platform APIs early.
Step 3: Establish a CI Pipeline
KMP requires building for multiple targets, which can double or triple your CI time. Set up a pipeline that caches dependencies and runs tests on each target separately. Use Gradle's build cache to avoid rebuilding unchanged modules. One team reported that their CI time dropped from 45 to 12 minutes after optimizing their Gradle configuration.
Step 4: Gradually Expand
Once the shared module is stable, add more logic: business rules, data repositories, and use cases. Keep the UI layer completely native. At each step, measure the percentage of shared code and the number of expect/actual declarations. If the ratio of platform-specific code grows too high, reconsider whether that module is worth sharing.
Tools, Stack, and Maintenance Realities
Choosing the right tooling is critical for KMP success. The ecosystem is still maturing, and some libraries are more KMP-friendly than others.
Recommended Stack
For networking, Ktor is the most widely used KMP-compatible client. For serialization, kotlinx.serialization works across all targets. For dependency injection, Koin has KMP support, while Dagger/Hilt does not (though manual DI or a service locator pattern can suffice). For local storage, SQLDelight provides a KMP-compatible SQLite wrapper. For testing, the standard Kotlin test framework works, but you may need to run tests on each target separately.
Maintenance Overhead
KMP adds a new dimension to dependency management: you must ensure every library in the shared module has KMP-compatible versions. This can lag behind the latest releases on Android or iOS. Teams often find themselves pinned to older library versions because the KMP variant is not yet updated. Additionally, the Kotlin compiler itself evolves rapidly, and upgrading Kotlin versions can require updating all KMP libraries simultaneously—a non-trivial coordination effort.
Build System Complexity
Gradle is the build system for KMP, but configuring it for iOS targets requires additional plugins and scripts. The Xcode integration (via an embedded framework) works but adds steps to the build process. Debugging build failures often requires understanding both Gradle and Xcode build systems, which can be a steep learning curve for developers.
Growth Mechanics: Scaling KMP Across Teams and Projects
As your organization adopts KMP more broadly, new challenges emerge around code ownership, versioning, and cross-team collaboration.
Modularization Strategy
Treat the shared module as an internal library with its own versioning and release cycle. Use semantic versioning and maintain a changelog to communicate breaking changes to consuming apps. This allows the Android and iOS teams to update at their own pace, rather than being forced to synchronize releases.
Cross-Team Communication
KMP blurs the line between platform teams. Establish a shared module governance model: who can approve changes to the common code? How are expect/actual additions reviewed? Without clear ownership, the shared module can become a dumping ground for platform-specific workarounds, reducing its value. One approach is to have a dedicated KMP team that owns the shared module and reviews all changes, while platform teams focus on UI and platform integrations.
Onboarding New Developers
New hires need to understand both Kotlin and the platform's native language. Provide a KMP onboarding guide that covers expect/actual patterns, memory management differences, and the build system. Pair programming between Android and iOS developers during the first shared module integration can build shared context and reduce silos.
Risks, Pitfalls, and Mitigations
Even with careful planning, KMP projects encounter common pitfalls. Awareness of these can save months of rework.
Pitfall 1: Over-Sharing UI Logic
Some teams try to share ViewModels or Presenters across platforms, but these often contain platform-specific lifecycle or threading assumptions. The result is a tangled mess of expect/actual declarations. Mitigation: keep UI logic platform-specific. Use the shared module only for pure business logic and data transformation. If you need to share state management, use a unidirectional architecture (e.g., MVI) with platform-agnostic state classes.
Pitfall 2: Ignoring iOS Memory Management
Kotlin/Native on iOS uses ARC, and objects passed to Swift may be retained longer than expected. This can cause memory leaks if shared objects hold strong references to platform objects. Mitigation: use weak references when passing platform objects into the shared module, and avoid storing platform callbacks in shared singletons. Profile memory on iOS regularly during development.
Pitfall 3: Underestimating Build Times
Building for both Android and iOS can take 2–3 times longer than building for a single platform. This slows down the feedback loop. Mitigation: use Gradle's parallel builds, invest in CI runners with sufficient resources, and consider using a remote build cache. Some teams run only the relevant target's tests during development, with full cross-platform builds only on CI.
Pitfall 4: Library Version Mismatches
KMP libraries often lag behind their platform-specific counterparts. A bug fix on Android may not be available in the KMP version for weeks. Mitigation: maintain a compatibility matrix of library versions, and have a fallback plan (e.g., using platform-specific implementations via expect/actual) if a critical fix is delayed. Consider contributing to open-source KMP libraries to accelerate fixes.
Decision Checklist and Mini-FAQ
Use the following checklist to evaluate whether KMP is a good fit for your project. Each item includes a brief rationale.
Decision Checklist
- Do you have at least two target platforms (e.g., Android + iOS)? KMP's value grows with the number of platforms. A single-platform project gains little.
- Is your business logic relatively stable? Rapidly changing logic may be easier to implement independently on each platform than to maintain expect/actual wrappers.
- Can your team tolerate library version lag? If you need the latest APIs on day one, KMP may not be suitable.
- Do you have Kotlin expertise on both platforms? iOS developers need to learn Kotlin, at least for the shared module.
- Is your app UI-heavy with little logic? KMP may not provide significant sharing. Consider Flutter or React Native if UI sharing is more important.
Frequently Asked Questions
Q: Can I use KMP with an existing native app? Yes, but the integration effort depends on how tightly coupled your existing code is. Start with a new feature or module that is relatively isolated. Incremental adoption is possible but requires careful refactoring.
Q: Does KMP support iOS frameworks like SwiftUI? KMP does not directly support SwiftUI; you call the shared module from Swift code. The UI remains fully native. You can use KMP to provide data and business logic to SwiftUI views.
Q: How does testing work in KMP? You can write unit tests in the common module that run on all targets. However, tests that depend on platform APIs (e.g., file system) require platform-specific test source sets. Use the same expect/actual pattern for test dependencies.
Q: What is the performance impact? For business logic, the overhead is negligible. The shared code is compiled to native code on iOS and JVM bytecode on Android. The main performance concerns come from serialization and interop calls between Kotlin and Swift, which are generally fast but can add latency if called in tight loops.
Synthesizing Your KMP Strategy
Kotlin Multiplatform is not a silver bullet, but it is a pragmatic tool for reducing duplication in business logic while preserving native UI quality. The key to success is a phased, honest approach: start small, measure sharing ratios, and be prepared to keep some logic platform-specific. The efficiency gains come from eliminating parallel implementations of the same logic, not from sharing everything.
Next Steps
- Run a pilot with a single shared module (e.g., networking) on a low-risk feature.
- Set up CI for both platforms and establish a performance baseline.
- Review after one sprint: compare development velocity, bug rates, and developer satisfaction against your previous approach.
- Scale gradually: add more shared modules only if the pilot shows clear benefits.
Remember that KMP is a means to an end, not an end in itself. The goal is to deliver better apps faster, not to maximize code sharing percentages. By focusing on the areas where sharing provides the highest return—stable business logic, data layers, and validation—you can unlock cross-platform efficiency without sacrificing platform excellence.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!