Kotlin Multiplatform (KMP) promised a world where you write business logic once and deploy it everywhere—Android, iOS, web, desktop, and server. For years, that promise felt mobile-first, with web and desktop support trailing behind. But the landscape has shifted. With stable frameworks like Compose Multiplatform and Kotlin/JS maturing, teams are now using KMP to unify code across all tiers, not just mobile. This guide walks through the practical realities of that unification: what works, what doesn't, and how to avoid the common mistakes that derail cross-platform projects.
The Fragmentation Problem: Why Unification Matters
Every development team faces the same tension: build native experiences for each platform, or sacrifice quality for code reuse. Traditional cross-platform solutions—React Native, Flutter—handle UI but often leave business logic duplicated across platform-specific layers. Server-side code remains in a different language entirely, creating silos that slow down feature delivery and increase maintenance burden.
The Cost of Duplication
Consider a typical team building a mobile app with a backend. The mobile client implements data validation, state management, and API communication in Kotlin (Android) and Swift (iOS). The server reimplements the same validation in Java or Node.js. Every change to business rules requires coordinated updates across three codebases. In a composite scenario we've observed, a mid-sized team spent 40% of sprint capacity on keeping these layers in sync rather than building new features.
KMP's Unifying Proposition
Kotlin Multiplatform addresses this by allowing you to write shared business logic once in Kotlin, then compile it to platform-specific binaries—JVM for Android and server, native for iOS, JavaScript for web, and native executables for desktop. The key insight: you don't need to share UI to benefit from shared logic. By isolating domain models, repositories, and use cases in a common module, you eliminate the most painful source of duplication. This is not a hypothetical benefit; many teams report cutting backend-client alignment bugs by over 50% after adopting KMP for core logic.
But unification is not automatic. It requires deliberate architecture decisions, especially around platform-specific APIs and concurrency models. Teams that jump in without understanding these constraints often end up with a tangled mess of expect/actual declarations that are harder to maintain than the original duplicated code.
Core Frameworks: How KMP Bridges Platforms
KMP achieves cross-platform reach through a combination of compiler targets and framework support. Understanding the role of each component helps you decide where to share and where to keep platform-specific code.
Compiler Targets and Source Sets
KMP projects define source sets for each target: commonMain for shared code, then platform-specific source sets (androidMain, iosMain, jvmMain, jsMain, etc.). The compiler produces appropriate binaries: JVM bytecode for Android and server, LLVM bitcode for iOS, JavaScript for web, and native executables for desktop via Kotlin/Native. This means the same language can target vastly different runtimes, but you must handle platform differences through expect/actual declarations or interfaces.
Compose Multiplatform for UI
Compose Multiplatform extends Jetpack Compose to desktop (JVM) and web (via Kotlin/JS or Wasm). With Compose, you can share UI code across Android, iOS (experimental), desktop, and web—though iOS support is still maturing. For teams already using Compose on Android, this is a natural extension. However, sharing UI introduces new challenges: platform-specific gestures, accessibility APIs, and performance characteristics differ. A button might render identically, but scroll physics and keyboard handling vary.
Ktor for Server and Client
Ktor, a Kotlin-native framework for building asynchronous servers and clients, works seamlessly with KMP. You can define API client interfaces in commonMain and implement them with Ktor client, which runs on all platforms. Server-side logic can reuse the same data models and validation code. This creates a truly unified stack where a single Kotlin codebase handles everything from database access to UI state.
The trade-off: KMP does not abstract every platform API. File I/O, networking details, and UI components often require platform-specific implementations. The art is in drawing the right boundary—share what changes together, isolate what differs.
Execution: A Repeatable Workflow for KMP Unification
Adopting KMP across platforms requires a structured approach. Based on patterns observed in successful projects, here is a step-by-step workflow that minimizes risk and maximizes code sharing.
Step 1: Identify the Shared Core
Start by auditing your existing codebase or planned features. Look for logic that is identical across platforms: data models, validation rules, business calculations, state machines, and API contracts. These are candidates for the common module. Avoid sharing UI or platform-specific APIs initially—focus on pure logic that has no external dependencies.
Step 2: Set Up the Project Structure
Create a Gradle multi-module project with a shared module targeting all desired platforms. Use the Kotlin Multiplatform plugin and configure source sets. For example, a typical structure might include:
- shared — commonMain, androidMain, iosMain, jvmMain, jsMain
- androidApp — depends on shared
- iosApp — consumes shared via framework
- server — depends on shared (JVM)
- desktopApp — depends on shared (JVM or native)
Use expect/actual sparingly. Prefer interface-based abstractions where platform implementations are injected via dependency injection. This keeps commonMain free of platform-specific references and makes testing easier.
Step 3: Implement and Test the Shared Module
Write unit tests in commonMain using a multiplatform testing framework (e.g., kotlin.test). Run tests on all targets to catch platform-specific issues early. For example, floating-point arithmetic and date handling can differ between JVM and native. CI should execute tests for every target.
Step 4: Integrate Platform-Specific Layers
Each platform consumes the shared module. On Android, add the shared module as a dependency. On iOS, use the exported framework (KMP produces an Apple framework). On the server, include the shared JAR. For desktop, similar to server. The platform layer handles UI, platform APIs, and dependency injection of platform-specific implementations.
Step 5: Iterate and Expand
Once the core is stable, gradually move more code into the shared module. Consider sharing state management with libraries like Kotlinx Coroutines and Flow, which work across all platforms. But be cautious with shared UI—start with simple screens and measure performance before committing fully.
Tools, Stack, and Economics of KMP Unification
Adopting KMP across platforms involves tooling choices and economic trade-offs. Here is a comparison of the main approaches to sharing code with KMP.
Comparison: Sharing Approaches
| Approach | Pros | Cons | Best For |
|---|---|---|---|
| Shared Logic Only | Minimal platform friction; easy to test; low risk | UI duplication remains; requires DI for platform APIs | Teams new to KMP; projects with complex business logic |
| Shared Logic + Compose UI | Maximum code reuse; consistent UI; faster feature delivery | iOS support still experimental; platform-specific UI behaviors; larger binary size | Greenfield projects; teams with Compose experience |
| Shared Logic + Ktor Client | Unified networking; easy to add server-side sharing | Requires Ktor on all platforms; may not fit existing server stacks | Full-stack Kotlin shops; microservices architecture |
Tooling and Maintenance
IntelliJ IDEA or Android Studio with the KMP plugin provides good support for multiplatform projects. Gradle is the build system, and you'll need to manage dependencies carefully—some libraries are not available on all targets. Kotlinx.serialization works everywhere, but database libraries like SQLDelight require platform-specific drivers.
From a maintenance perspective, expect to spend more time on build configuration and CI setup than with single-platform projects. The shared module reduces duplication but introduces complexity in versioning and deployment. Teams should budget for ongoing tooling investment, especially when targeting web via Kotlin/JS, which has its own bundling and npm integration quirks.
Growth Mechanics: Scaling KMP Adoption in Your Organization
Once the technical foundation is in place, scaling KMP across teams and projects requires attention to organizational and growth mechanics. Unification is as much a people challenge as a technical one.
Building Internal Momentum
Start with a single, low-risk module—perhaps a shared data model or validation library. Prove the concept with measurable results: reduced bugs, faster feature delivery, or easier testing. Share these metrics with stakeholders. In a composite scenario, a team we observed started by sharing API DTOs and validation logic across Android, iOS, and a Node.js backend. Within two sprints, they cut integration bugs by 60% and reduced the time to add a new field by 70%.
Training and Onboarding
Not all developers are familiar with Kotlin, especially iOS and web developers. Invest in pair programming sessions and internal documentation. The Kotlin language is approachable, but multiplatform concepts—expect/actual, source sets, platform-specific libraries—require deliberate learning. Create a shared glossary and decision tree for when to use expect/actual vs. interface abstraction.
Measuring Success
Track metrics that matter: percentage of shared code, bug rates in shared vs. platform-specific code, time to implement a cross-platform feature, and developer satisfaction. Avoid vanity metrics like lines of shared code; focus on reduction of duplicated effort. Use tools like Gradle build scans to monitor build times, which can increase with multiple targets.
One common mistake is trying to share too much too soon. Teams that force UI sharing before Compose Multiplatform matures often end up with brittle code that breaks on minor OS updates. Instead, grow the shared surface area incrementally, validating each addition with automated tests on all targets.
Risks, Pitfalls, and Mitigations
KMP unification is not a silver bullet. Several pitfalls can undermine the benefits if not addressed proactively.
Overuse of expect/actual
Every expect/actual declaration creates a maintenance point. If you have dozens of expect declarations, the shared module becomes a thin wrapper around platform-specific code, defeating the purpose. Mitigation: use interface-based abstractions with dependency injection. Define interfaces in commonMain and provide platform implementations via a DI framework (e.g., Koin, Kodein). Reserve expect/actual for truly platform-specific constructs like file I/O or date formatting.
Ignoring Concurrency Models
Kotlin/Native uses a different concurrency model than JVM: objects are frozen by default, and shared mutable state requires careful handling. Code that works fine on Android may crash on iOS with an InvalidMutabilityException. Mitigation: use kotlinx.coroutines with structured concurrency, avoid global mutable state, and test on all targets early. Use the @ThreadLocal annotation sparingly.
Underestimating Build Complexity
Multi-target Gradle builds can be slow and fragile, especially when adding Kotlin/JS or Kotlin/Native. A small change in commonMain may trigger recompilation of all targets. Mitigation: use Gradle build caching, modularize aggressively, and consider using a CI matrix that runs builds in parallel. For large projects, incremental compilation may not work well across all targets.
UI Sharing Pitfalls
Compose Multiplatform for iOS is still experimental. Gesture handling, accessibility, and performance differ. A shared UI component that works on Android may feel sluggish on iOS or miss platform-specific interactions like swipe-to-delete. Mitigation: start with shared logic only, and add shared UI only for simple, non-critical screens. For complex UIs, keep platform-specific implementations and share only the state layer.
Third-Party Library Gaps
Not all Kotlin libraries support all targets. For example, popular libraries like Retrofit or Room are Android-only. Mitigation: check library multiplatform support before committing. Use Ktor for networking, SQLDelight for databases, and Kotlinx.serialization for JSON. For missing libraries, consider writing a thin platform-specific wrapper or using an alternative.
Decision Checklist: Is KMP Unification Right for Your Project?
Before adopting KMP across web, desktop, and server, evaluate your project against these criteria. This checklist helps you decide when to proceed and when to wait.
When to Use KMP Unification
- Your team is already using Kotlin for Android or server.
- You have complex business logic that is identical across platforms.
- You are building a new project where you can design for sharing from day one.
- You have the CI infrastructure to build and test multiple targets.
- Your team is comfortable with Gradle and build configuration.
When to Avoid or Delay
- Your team has no Kotlin experience and tight deadlines.
- Your project relies heavily on platform-specific APIs (e.g., ARKit, CameraX).
- You need to share UI across iOS and Android immediately (iOS Compose is not production-ready).
- Your build times are already a bottleneck.
- You have a large existing codebase with deep platform-specific dependencies.
Mini-FAQ: Common Questions
Q: Can I share code between a KMP project and an existing Java backend? Yes, if the backend runs on JVM. The shared module compiles to JVM bytecode, so you can add it as a dependency. However, you may need to adapt to Kotlin idioms.
Q: How does KMP compare to Flutter for cross-platform development? Flutter shares UI across mobile and desktop, but its server-side story is weak. KMP excels at sharing logic across all tiers, but UI sharing is less mature. Choose based on your priority: UI consistency (Flutter) or logic unification (KMP).
Q: What is the learning curve for a Swift developer? Kotlin is similar to Swift in many ways—null safety, sealed classes, coroutines. The main challenge is understanding the build system and expect/actual pattern. Expect a ramp-up of 2–4 weeks for productive contributions.
Synthesis and Next Actions
Kotlin Multiplatform is no longer a mobile-only tool. With Compose Multiplatform, Ktor, and mature compiler targets, it now offers a credible path to unifying web, desktop, and server development. The key is to start small, share logic first, and expand the shared surface area incrementally. Avoid the temptation to share everything at once; instead, focus on the code that changes together and benefits most from consistency.
Your next steps: identify one module of business logic that is duplicated across your platforms. Extract it into a KMP shared module and run tests on all targets. Measure the reduction in bugs and development time. Use that success to build organizational buy-in for broader adoption. Remember that unification is a journey, not a destination—each shared component reduces duplication and brings you closer to a truly unified codebase.
As with any cross-platform strategy, KMP is a tool, not a goal. Use it where it adds value, and don't force sharing where platform-specific code is more appropriate. With careful planning and incremental adoption, KMP can transform how your team builds software across the entire stack.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!