Kotlin Multiplatform (KMP) has moved beyond the experimental phase to become a production-ready approach for sharing business logic across Android, iOS, web, and desktop. However, many teams find that the initial excitement of code sharing gives way to complexity when they face platform-specific quirks, dependency management, and testing strategies. This guide distills advanced practices from real-world implementations, focusing on architectural decisions that determine long-term maintainability and developer productivity. We assume familiarity with basic KMP setup and dive into the trade-offs that separate smooth projects from those that stall.
Why KMP Still Challenges Teams: The Gap Between Promise and Practice
KMP’s core value proposition is clear: write shared logic once, reuse it across platforms. Yet practitioners often report that the first few months involve unexpected friction. The shared module can become a dumping ground for code that is neither fully platform-agnostic nor properly abstracted. One common scenario involves a team that starts by sharing networking and data models, then attempts to share UI state management, only to find that iOS threading models and Android’s lifecycle impose different constraints. The result is a proliferation of expect/actual declarations that are hard to maintain.
Another frequent pain point is build configuration. Gradle setup for KMP is more involved than for a single-platform project, and mismatched Kotlin versions between shared and platform modules can lead to cryptic errors. Teams often underestimate the effort required to set up continuous integration for multiple targets, especially when using Apple Silicon vs. Intel runners. These challenges are not insurmountable, but they require deliberate planning.
The Expect/Actual Trap
The expect/actual mechanism is elegant in theory: declare a contract in common code and provide platform-specific implementations. In practice, it tempts developers to leak platform APIs into shared code, creating a tangled web of conditional logic. A better approach is to define thin interfaces in the shared module and implement them fully on each platform using dependency injection. This keeps the shared code pure and testable.
Composite Scenario: A Team That Overcame the Gap
Consider a team building a cross-platform fitness app. They initially placed all network calls and database operations in the shared module, using expect/actual for file storage and sensor access. As the app grew, they found that changes to the iOS sensor implementation required modifying the shared module’s expect declaration, causing rebuilds of the entire shared code. They refactored by moving platform-specific APIs behind a repository pattern injected via a common interface. This reduced build times by 40% and made unit testing straightforward.
Core Mechanisms: How KMP Actually Works Under the Hood
Understanding KMP’s compilation model is essential for advanced usage. KMP does not compile shared code to a universal binary; instead, it compiles to intermediate representations (Kotlin/Native for iOS, JVM bytecode for Android, JavaScript for web). This means that shared code must avoid calling platform APIs directly. The compiler enforces this through the common module’s limited standard library, which excludes platform-specific classes like java.io.File or Foundation.NSData.
The expect/actual mechanism is resolved at compile time. For each expect declaration in the common module, the compiler looks for a matching actual in the platform-specific source sets. If an actual is missing, the build fails. This design encourages a clean separation but also means that adding a new platform target requires implementing all actuals, which can be burdensome for large interfaces.
Compose Multiplatform: Sharing UI Without Sacrificing Native Feel
Compose Multiplatform extends KMP to UI, allowing teams to share not just logic but also UI components across Android, iOS, desktop, and web. The key advantage is that Compose’s declarative model maps well to multiplatform scenarios. However, performance on iOS remains a concern for complex layouts, as Compose renders through a Skia canvas rather than using native UIKit controls. Teams should profile early and consider using native UI for performance-critical screens, while sharing common screens like settings or onboarding.
Coroutines and Flow: The Backbone of Shared Async Code
Kotlin coroutines and Flow are first-class citizens in KMP. They provide a unified approach to asynchronous programming across platforms. One advanced technique is to use kotlinx.coroutines with a custom dispatcher that maps to the platform’s main thread (e.g., Dispatchers.Main on Android, DispatchQueue.main on iOS via a helper). This ensures that shared code can safely update UI without platform-specific boilerplate.
Execution: Building a Repeatable Workflow for KMP Projects
Successful KMP projects follow a disciplined workflow that prioritizes incremental adoption. Start by identifying the smallest piece of logic that can be shared—often data models, validation, or network clients. Build a shared module with a clear API surface, and avoid pulling in platform dependencies. Use a build tool like Gradle with the Kotlin Multiplatform plugin, and organize source sets by platform (commonMain, androidMain, iosMain, etc.).
Step-by-Step Guide to Setting Up a Shared Module
- Define the shared module’s scope: List all features that will be shared. Start with 2-3 core features to limit complexity.
- Create the module structure: Use the KMP wizard or manually create directories for commonMain, androidMain, iosMain. Add dependencies like Ktor for networking and kotlinx.serialization for JSON.
- Implement expect/actual interfaces: For platform-specific operations (e.g., file storage), define an interface in commonMain and provide actual implementations in each platform source set.
- Set up dependency injection: Use a lightweight DI framework like Koin or manual constructor injection to provide platform implementations to shared code.
- Write unit tests: Test shared logic in commonTest using a test framework like kotlin.test. Mock platform interfaces using a mocking library like MockK.
- Integrate with platform apps: Add the shared module as a dependency in your Android and iOS projects. Use a framework like CocoaPods or Swift Package Manager for iOS integration.
Composite Scenario: A Team That Scaled Incrementally
A team building a note-taking app started by sharing only the data layer (room-based persistence on Android, Core Data on iOS behind a common repository interface). After three months, they added shared networking and authentication. Six months in, they introduced Compose Multiplatform for the settings screen. This incremental approach allowed them to validate each step without overhauling the entire codebase.
Tools, Stack, and Maintenance Realities
Choosing the right tooling is critical for KMP success. The Kotlin Multiplatform plugin, along with Ktor for networking, kotlinx.serialization for data serialization, and SQLDelight for database access, forms a robust stack. For UI, Compose Multiplatform is the primary option, though some teams prefer to keep native UIs and share only logic.
Comparison of Networking Libraries
| Library | Pros | Cons | Best For |
|---|---|---|---|
| Ktor | First-class KMP support, coroutine-native, flexible | Steeper learning curve, smaller ecosystem | Teams already using Kotlin coroutines |
| Retrofit (via KMP wrapper) | Familiar to Android developers, large community | Not officially supported for KMP, may lag behind | Migrating existing Android codebases |
| Apollo GraphQL | KMP support, strong typing for GraphQL | Requires GraphQL backend, more setup | Teams using GraphQL APIs |
Dependency Injection Options
Koin is the most popular DI framework for KMP because it is lightweight and works across platforms. Kodein-DI is another option with a similar philosophy. For teams that prefer compile-time safety, Dagger Hilt can be used on Android with a separate manual DI setup on iOS, but this adds complexity. A pragmatic approach is to use constructor injection with a simple service locator in the shared module, delegating platform-specific instances to the app module.
Build and CI Considerations
KMP builds are slower than single-platform builds because the compiler generates binaries for each target. Use Gradle build caching and parallel execution to speed up local builds. For CI, consider using a Mac with Apple Silicon for iOS compilation, and Docker images for Android. Tools like Gradle Enterprise can help diagnose build bottlenecks. Regularly update Kotlin and plugin versions to benefit from performance improvements.
Growth Mechanics: Scaling KMP Across Teams and Projects
As KMP adoption grows within an organization, maintaining consistency becomes a challenge. Establish coding conventions for shared modules, such as naming patterns for expect/actual declarations and rules for when to use platform-specific code. Use API linting tools like detekt with custom rules to enforce these conventions.
Modularization Strategies for Large Codebases
Break the shared module into smaller feature modules, each with its own commonMain and platform source sets. This reduces compilation times and allows teams to work independently. For example, a shared module for networking can be separate from one for analytics. Use Gradle’s composite builds to manage inter-module dependencies.
Managing Platform-Specific UI While Sharing Logic
Even when using Compose Multiplatform, some screens may need native UI for performance or platform-specific behaviors (e.g., Apple Pay integration). Define a contract in shared code for the screen’s state and events, and implement the UI natively on each platform. This pattern, sometimes called “shared state, native UI,” preserves the benefits of code sharing without sacrificing platform fidelity.
Composite Scenario: A Large E-Commerce App
A retail company with separate Android and iOS teams adopted KMP for their product catalog and checkout logic. They created a shared module for domain models, network calls, and business rules. Each platform team retained ownership of UI and platform-specific features like push notifications. The shared module was maintained by a small cross-platform team that reviewed all changes. Over a year, they reduced duplicate code by 60% and cut development time for new features by 30%.
Risks, Pitfalls, and Mitigations
KMP is not a silver bullet. Common pitfalls include over-sharing code that should remain platform-specific, underestimating the complexity of expect/actual maintenance, and neglecting performance profiling on iOS. Another risk is the “all or nothing” mindset—teams that try to share everything from day one often abandon KMP when they hit roadblocks.
Pitfall 1: Leaking Platform APIs into Shared Code
When a shared function needs a platform-specific parameter, it is tempting to add an expect declaration that exposes the platform type. This couples shared code to platform APIs. Mitigation: wrap platform types in a common interface and pass them through DI. For example, instead of expect fun getSharedPreferences(): SharedPreferences, define an interface PreferencesRepository in commonMain and implement it on each platform.
Pitfall 2: Ignoring iOS Performance
Kotlin/Native on iOS has some overhead compared to Swift, especially for memory-intensive operations. Use tools like the Kotlin/Native memory manager (now based on a new allocator) and profile with Instruments. Avoid creating large object graphs in shared code that are called frequently from UI loops. Consider using Swift for UI-level logic and calling into KMP for business operations.
Pitfall 3: Build Configuration Drift
As the project evolves, build files can become inconsistent, leading to mysterious failures. Mitigation: use version catalogs in Gradle to centralize dependency versions, and run CI builds for all targets on every pull request. Automate the generation of iOS framework integration (e.g., via XCFramework) to avoid manual steps.
Pitfall 4: Testing Complexity
Unit testing shared code is straightforward, but integration testing across platforms is harder. Use a test doubles pattern for platform interfaces, and consider using a shared test module that runs on both JVM and native targets. For UI testing, rely on platform-specific frameworks (Espresso for Android, XCTest for iOS) and test the shared logic separately.
Decision Checklist: When to Use KMP and When to Avoid It
This checklist helps teams evaluate whether KMP fits their project. Answer each question honestly; if most answers are “no,” consider alternative approaches like Flutter or React Native.
- Do you have a significant amount of business logic that is identical across platforms? KMP shines when 30% or more of your code can be shared.
- Is your team proficient in Kotlin? Learning Kotlin from scratch for KMP adds overhead; teams with existing Kotlin experience adapt faster.
- Are you building a new project from scratch? Greenfield projects are easier to structure for KMP than migrating existing codebases.
- Do you need access to platform-specific APIs extensively? If your app relies heavily on hardware features (camera, Bluetooth, ARKit), KMP may add more complexity than it saves.
- Is your team size large enough to dedicate resources to shared module maintenance? A small cross-platform team can maintain the shared module, but if every developer works on both platforms, coordination costs rise.
Mini-FAQ: Common Questions
Q: Can I use KMP with an existing native iOS app? Yes, by adding a shared module as a framework dependency. However, expect some integration effort for bridging Swift and Kotlin types.
Q: How does KMP compare to Flutter? KMP shares logic while allowing native UI; Flutter shares both logic and UI but uses a custom rendering engine. Choose KMP if you want platform-specific UI and have Kotlin expertise.
Q: Is KMP stable for production? As of May 2026, KMP is stable and used in production by companies like Netflix and McDonald’s. However, some libraries (e.g., for analytics) may have immature KMP support.
Q: What is the learning curve for a team new to KMP? Expect 2-4 weeks for a team familiar with Kotlin to become productive. iOS developers may need additional time to understand Kotlin/Native and the Gradle build system.
Synthesis: Building a Sustainable KMP Strategy
Mastering KMP requires a shift in mindset from “share everything” to “share what makes sense.” The most successful teams treat the shared module as a library that provides a well-defined API, not as a monolithic container for all logic. They invest in build infrastructure, write comprehensive tests for shared code, and accept that some features will always be platform-specific.
Start small, measure the benefits, and iterate. A good initial goal is to share data models and network code, which typically yields a 20-30% reduction in duplicate code. As confidence grows, expand to shared business logic and, if appropriate, shared UI with Compose Multiplatform. Regularly review the cost of maintaining expect/actual declarations against the savings from code sharing.
Finally, stay engaged with the KMP community. The ecosystem evolves rapidly, with new libraries and tooling improvements appearing regularly. Attend Kotlin conferences, follow JetBrains’ blog, and contribute to open-source KMP projects. By combining a pragmatic approach with continuous learning, your team can harness KMP’s power without falling into its traps.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!