Skip to main content
Kotlin Multiplatform Projects

Mastering Kotlin Multiplatform: Advanced Strategies for Seamless Cross-Platform Development

Kotlin Multiplatform (KMP) has matured into a viable solution for sharing business logic across Android, iOS, web, and desktop. However, teams often encounter friction when moving beyond toy examples: architecture decisions, platform-specific APIs, and testing strategies can make or break a project. This guide distills advanced strategies from composite scenarios and practitioner experience, focusing on what works in production and what does not. Last reviewed: May 2026.Why Shared Logic Still Fails: Common Pain Points in KMP AdoptionMany teams adopt KMP expecting a write-once-run-anywhere utopia, but the reality involves careful design. The core challenge is not sharing code—it is managing the boundary between shared and platform-specific logic. In a typical project, a team might share data models, network clients, and domain logic, while keeping UI and platform APIs (e.g., camera, sensors) in native code. Without clear boundaries, shared code can become tangled with platform dependencies, leading to fragile builds and slow

Kotlin Multiplatform (KMP) has matured into a viable solution for sharing business logic across Android, iOS, web, and desktop. However, teams often encounter friction when moving beyond toy examples: architecture decisions, platform-specific APIs, and testing strategies can make or break a project. This guide distills advanced strategies from composite scenarios and practitioner experience, focusing on what works in production and what does not. Last reviewed: May 2026.

Why Shared Logic Still Fails: Common Pain Points in KMP Adoption

Many teams adopt KMP expecting a write-once-run-anywhere utopia, but the reality involves careful design. The core challenge is not sharing code—it is managing the boundary between shared and platform-specific logic. In a typical project, a team might share data models, network clients, and domain logic, while keeping UI and platform APIs (e.g., camera, sensors) in native code. Without clear boundaries, shared code can become tangled with platform dependencies, leading to fragile builds and slow iteration.

The Expect/Actual Trap

The expect/actual mechanism is powerful but often misused. A common mistake is to declare a large expect class with many functions, then provide actual implementations for each platform. This creates a maintenance burden: every change to the expect declaration requires updating all actual implementations. A better approach is to keep expect declarations minimal—ideally, small interfaces or single functions—and use dependency injection to supply platform implementations. For example, instead of an expect class for a database driver, define an interface in shared code and inject the platform-specific driver via a factory.

Composite Scenario: A Social Media App

Consider a social media app with shared networking, caching, and authentication logic. The team initially used expect/actual for the entire networking stack, but found that Android and iOS networking libraries (OkHttp vs. Ktor-Native) required different configuration. They refactored to use a shared interface for HTTP calls, with platform-specific factories provided through a dependency injection framework. This reduced actual declarations from 15 to 3 and made testing easier—mock implementations could be injected without touching platform code.

Another pain point is build configuration. Gradle setup for KMP can be complex, especially when targeting iOS. Many teams struggle with framework linking, CocoaPods integration, and Xcode build phases. A systematic approach is to use the Kotlin Multiplatform Gradle plugin with a clear module structure: each shared module should have a single responsibility (e.g., network, domain, data) and minimal platform dependencies. Avoid monolithic shared modules that try to do everything.

Core Architecture Patterns: Structuring Your Shared Code for Maintainability

Choosing the right architecture for KMP is critical. The most common patterns are Clean Architecture, Model-View-ViewModel (MVVM), and Redux-like unidirectional data flow. Each has trade-offs for shared code.

Clean Architecture in KMP

Clean Architecture separates code into layers: data, domain, and presentation. In KMP, the domain layer (entities, use cases) is fully shared and platform-agnostic. The data layer can be shared for repositories and data sources, but platform-specific implementations (e.g., database drivers, file storage) are injected. The presentation layer is typically platform-specific (SwiftUI for iOS, Jetpack Compose for Android). This pattern works well for complex apps with rich business logic, as it enforces separation of concerns and testability.

MVVM with Shared ViewModels

MVVM is popular for mobile apps, and KMP allows sharing ViewModels across platforms. The ViewModel contains state and business logic, while the View (UI) observes state changes. Kotlin coroutines and StateFlow are ideal for this. However, platform-specific UI frameworks require different state observation mechanisms: Android can use LiveData or StateFlow directly, while iOS needs a wrapper (e.g., using Combine or a simple observer). A common approach is to expose shared ViewModels as observable objects via a bridge library like SKIE or custom wrappers.

Unidirectional Data Flow (UDF)

UDF patterns like Redux or MVI (Model-View-Intent) are also feasible in KMP. They enforce a strict data flow: intents (actions) are processed by a reducer that updates state. This pattern is highly testable and works well for apps with complex state management. However, it can introduce boilerplate. Teams should weigh the benefits against the overhead, especially for smaller apps.

In practice, many teams adopt a hybrid: shared domain and data layers with platform-specific presentation. The key is to define clear interfaces at the boundary. Use Kotlin interfaces and sealed classes for shared models, and avoid leaking platform types (e.g., Android Context) into shared code.

Execution Workflows: From Development to Deployment

Setting up a smooth development workflow for KMP requires attention to tooling, build scripts, and CI/CD. The following steps outline a repeatable process.

Step 1: Module Structure and Gradle Configuration

Start with a multi-module project. Create a shared module for common code, and platform-specific modules for Android and iOS. Use the Kotlin Multiplatform Gradle plugin with source sets: commonMain, androidMain, iosMain. Configure dependencies carefully—use api vs. implementation to avoid leaking transitive dependencies. For iOS, use the framework configuration to produce an XCFramework or embed the shared framework via CocoaPods or Swift Package Manager.

Step 2: Dependency Injection Setup

Choose a DI framework that supports KMP. Koin and Kodein-DI are popular choices because they are pure Kotlin and work on all platforms. Define modules for each layer (network, database, repositories) and provide platform-specific implementations via expect/actual factories or interface-based injection. Avoid using Android-specific DI frameworks like Dagger in shared code.

Step 3: Testing Strategy

Unit tests can be written in commonTest using Kotlin test frameworks (kotlin.test, MockK). For integration tests that require platform APIs, use expect/actual to provide test doubles. For example, create an expect class for file storage and provide an in-memory implementation in commonTest. UI testing remains platform-specific, but shared ViewModels can be tested independently.

Step 4: CI/CD Integration

Configure CI to run common tests on all platforms. For iOS, you need a macOS runner with Xcode installed. Use Gradle tasks like iosTest and androidTest. For deployment, automate building the iOS framework and integrating it into the Xcode project. Tools like Fastlane can help with code signing and app store uploads.

Tools, Stack, and Maintenance Realities

Selecting the right tools is crucial for long-term maintainability. The KMP ecosystem includes libraries for networking (Ktor), serialization (kotlinx.serialization), coroutines, and multiplatform storage (SQLDelight, Realm). Each has trade-offs.

Networking: Ktor vs. Retrofit

Ktor is the primary choice for KMP because it is multiplatform and supports coroutines. It provides client engines for each platform (OkHttp on Android, Darwin on iOS). Retrofit is Android-only, so it cannot be used in shared code. However, you can use Ktor with a Retrofit-like interface via Ktor's resource or API client plugins. For teams migrating from Retrofit, the learning curve is moderate.

Database: SQLDelight vs. Realm

SQLDelight generates type-safe Kotlin code from SQL statements and supports KMP natively. It works well for relational data and is easy to test. Realm Kotlin SDK is also multiplatform but uses a different data model (object-based). SQLDelight is generally preferred for its simplicity and SQL familiarity, while Realm is better for real-time sync and complex object graphs.

Serialization: kotlinx.serialization

kotlinx.serialization is the standard for KMP. It supports JSON, CBOR, and protocol buffers. Use it for network responses, local storage, and shared data models. Avoid Gson or Moshi in shared code as they are JVM-only.

Maintenance Considerations

KMP libraries evolve rapidly. Regularly update dependencies and test on both platforms. Use version catalogs in Gradle to manage versions centrally. Plan for breaking changes in Kotlin versions (e.g., Kotlin 2.0+ introduces new compiler features). Allocate time for refactoring expect/actual declarations as the platform APIs evolve.

Growth Mechanics: Scaling Your KMP Codebase

As your project grows, maintaining code quality and team velocity becomes challenging. Here are strategies for scaling.

Modularization

Split shared code into multiple modules by feature or layer. For example, have separate modules for network, database, analytics, and domain. This reduces compilation time and allows teams to work independently. Use Gradle's build cache and parallel builds to speed up CI.

Code Generation and Annotation Processing

KMP supports annotation processing via Kotlin Symbol Processing (KSP). Use it to generate boilerplate code like DI modules, serializers, or database schemas. For example, Room (Android) is not multiplatform, but SQLDelight uses KSP to generate code from .sq files.

Performance Optimization

Shared code can introduce overhead if not optimized. Avoid excessive object allocations in hot paths. Use inline functions and value classes where appropriate. For iOS, be mindful of memory management: Kotlin objects are garbage-collected on Android but reference-counted on iOS (via Kotlin/Native). Leak detection tools like LeakCanary are Android-only; on iOS, use Instruments to profile memory.

Composite Scenario: A Fintech App

A fintech app with shared logic for transaction processing, fraud detection, and notification scheduling grew to 50+ shared modules. The team adopted a layered architecture with strict dependency rules: domain modules could not depend on data modules directly; instead, they used interfaces implemented by data modules. They also used KSP to generate API client stubs from OpenAPI specs, reducing manual coding. This modular approach allowed them to onboard new features without breaking existing ones.

Risks, Pitfalls, and Mitigations

Even experienced teams encounter pitfalls. Here are the most common and how to avoid them.

Pitfall 1: Overusing expect/actual

As mentioned earlier, excessive expect/actual declarations create maintenance burden. Mitigation: prefer interfaces + DI over expect/actual for complex APIs. Use expect/actual only for truly platform-specific primitives (e.g., UUID generation, current time).

Pitfall 2: Ignoring iOS Performance

Kotlin/Native on iOS can have performance issues if not optimized. Common causes: excessive object allocation, large collections, and heavy use of reflection. Mitigation: profile iOS builds early, use primitive types where possible, and avoid kotlin.reflect in shared code.

Pitfall 3: Inconsistent Error Handling

Shared code often uses sealed classes for results (Success/Failure), but platform-specific error handling can differ. Mitigation: define a common error hierarchy in shared code and map platform errors to it. Use Kotlin's Result type carefully—it is not designed for all error scenarios.

Pitfall 4: Build Configuration Complexity

Gradle configuration for KMP can become tangled, especially with multiple targets. Mitigation: use a buildSrc or convention plugins to share configuration. Keep Gradle files concise and document non-obvious settings.

Pitfall 5: Testing Gaps

Teams often neglect testing platform-specific actual implementations. Mitigation: write unit tests for actual implementations using platform-specific test runners (e.g., XCTest for iOS, JUnit for Android). Use integration tests for end-to-end flows.

Decision Checklist: When to Use KMP and When Not To

This mini-FAQ addresses common questions teams face when considering KMP.

Should we use KMP for a new project?

Yes, if your team has Kotlin expertise and you need to share business logic across Android and iOS. KMP is mature enough for production use. However, if your app relies heavily on platform-specific UI or hardware features (e.g., ARKit, CameraX), you may still need significant native code.

Can we share UI code?

KMP does not natively share UI. Compose Multiplatform allows sharing UI across Android, iOS, and desktop, but it is still evolving. For production apps, many teams prefer native UI for each platform and share only logic.

How do we handle platform-specific APIs like camera or GPS?

Use expect/actual for thin wrappers that call platform APIs. Keep the shared interface simple (e.g., a function to take a photo) and implement it on each platform. For complex APIs, consider using a library like KMP-NativeCoroutines to bridge async calls.

What is the learning curve for iOS developers?

iOS developers familiar with Swift may need time to learn Kotlin syntax and tooling. However, Kotlin is similar to Swift in many ways. Provide training on Gradle, coroutines, and the KMP build process.

How do we manage dependency updates?

Use a version catalog in Gradle and schedule regular updates. Test on both platforms after each update. Subscribe to Kotlin's release notes for breaking changes.

Synthesis and Next Actions

Mastering Kotlin Multiplatform requires a strategic approach to architecture, tooling, and team practices. The key takeaways are: keep expect/actual declarations minimal, use dependency injection for platform-specific code, modularize your shared codebase, and invest in testing early. Avoid the temptation to over-share—sometimes native code is the right choice.

Concrete Next Steps

1. Audit your current KMP project for excessive expect/actual declarations and refactor to use interfaces where possible. 2. Set up a CI pipeline that runs tests on both Android and iOS. 3. Profile your iOS build for performance bottlenecks. 4. Create a shared error-handling strategy using sealed classes. 5. Evaluate whether Compose Multiplatform is suitable for your UI needs. 6. Schedule regular dependency updates and allocate time for migration.

By following these strategies, you can build a maintainable, scalable KMP codebase that delivers on the promise of seamless cross-platform development. For further reading, consult the official Kotlin documentation and community resources—always verify against the latest Kotlin version.

About the Author

This article was prepared by the editorial team for this publication. We focus on practical explanations and update articles when major practices change.

Last reviewed: May 2026

Share this article:

Comments (0)

No comments yet. Be the first to comment!