Skip to main content
Kotlin Multiplatform Projects

Kotlin Multiplatform: Sharing Code Between iOS and Android Without the Headaches

For years, mobile developers have faced the costly and time-consuming reality of maintaining two separate codebases for iOS and Android. Kotlin Multiplatform (KMP) emerges as a powerful, pragmatic solution, allowing you to share core business logic, data models, and networking code while preserving the native UI experience users expect. This comprehensive guide, based on extensive hands-on experience, cuts through the hype to provide a clear, practical roadmap. You'll learn the core concepts, understand the real-world benefits and trade-offs, and discover actionable strategies for integrating KMP into your projects. We'll explore specific use cases, common pitfalls to avoid, and how leading companies are successfully leveraging this technology to accelerate development, reduce bugs, and unify their engineering efforts without sacrificing platform quality.

Introduction: The Cross-Platform Dilemma and a Pragmatic Solution

As a mobile developer who has built and maintained apps for both major platforms, I know the frustration firsthand. You fix a critical bug in your Android networking layer, only to realize you must now replicate the exact same fix in Swift for iOS. This duplication is more than just an annoyance; it's a significant drain on resources, a source of inconsistent behavior, and a major hurdle to team velocity. For years, the promise of "write once, run anywhere" solutions often came with heavy compromises in performance, look, and feel. Kotlin Multiplatform (KMP) takes a fundamentally different and more pragmatic approach. Instead of trying to render UI across platforms, KMP focuses on what it does best: sharing the business logic, data models, and platform-agnostic code that forms the backbone of your application. This guide is born from implementing KMP in production environments. I'll show you how to leverage its strengths, navigate its current limitations, and build a robust, maintainable cross-platform foundation that truly delivers on the promise of shared code—without the traditional headaches.

Understanding the Kotlin Multiplatform Philosophy

KMP isn't a UI framework. It's a technology that allows you to compile Kotlin code to multiple target platforms: JVM (Android), native binaries (iOS), JavaScript, and even the desktop. This core distinction is crucial for understanding its value proposition and appropriate use cases.

Shared Logic, Native UI: The Core Tenet

The guiding principle of KMP is to write your business logic, data models, validation rules, and networking code once in Kotlin. This shared module then compiles to a platform-specific format: a JAR for Android and a framework (like .framework or .xcframework) for iOS. Your Android app uses this code directly as a Kotlin dependency. Your iOS app, written in Swift or Objective-C, consumes the generated framework. The UI layer on each platform remains fully native—you use Jetpack Compose or Views on Android and SwiftUI or UIKit on iOS. This ensures your app feels perfectly at home on each device, providing the performance and UX polish users demand.

How It Differs from Flutter and React Native

Flutter and React Native are excellent frameworks that solve a different problem: they provide a unified UI toolkit. KMP solves the logic-sharing problem. You can think of Flutter as providing the entire car (engine, chassis, and body), while KMP provides just the high-performance engine, letting you build the native chassis and body for each platform. This makes KMP an ideal choice for teams that have existing native UI expertise, large native codebases, or stringent platform-specific UI requirements but want to unify their underlying application intelligence.

The Role of Expect/Actual Declarations

To handle platform-specific needs (like reading a file, accessing a secure keystore, or getting device info), KMP uses a brilliant mechanism called `expect`/`actual` declarations. In your shared common code, you define an `expect` function or class. Then, in platform-specific source sets (e.g., `androidMain` and `iosMain`), you provide the `actual` implementation. The compiler wires it all together. For example, you might have an `expect fun getDeviceId(): String` in common code, with the Android implementation using `Build` APIs and the iOS implementation using `UIDevice`.

Setting Up Your First KMP Project: A Strategic Approach

Starting with KMP is easier than ever, thanks to improved tooling. However, a thoughtful setup is key to long-term success.

Choosing Your Project Structure

You have two primary options: a single multi-module project containing both Android and iOS modules, or separate repositories for the shared KMP module and the consuming native apps. For teams starting out, the single-project approach within Android Studio (with the KMM plugin) offers the best integrated experience. It allows you to write, run, and debug shared and Android code seamlessly. The iOS module in this setup is primarily for building the framework and running iOS unit tests.

Essential Gradle Configuration Insights

The `build.gradle.kts` file for your shared module is the control center. Key sections include defining your targets (`android()` and `iosX64()`, `iosArm64()`, `iosSimulatorArm64()`) and managing dependencies. Use `sourceSets` to declare dependencies that are specific to a platform. A critical best practice I've adopted is to use the `maven-publish` plugin to publish your shared module as an artifact (like an AAR or XCFramework) to a local Maven repository or your company's artifact server. This decouples the shared library development from the app teams and mirrors a real-world microservices-like consumption model.

Integrating with Xcode: The Bridge to iOS

This is often the point of initial friction. The KMP Gradle plugin can generate an XCFramework. You then add this .xcframework bundle to your Xcode project. Configure your iOS app's build phases to run the Gradle task that builds the framework before compilation. Tools like CocoaPods or Swift Package Manager (SPM) integration are also evolving and can streamline this process. In my projects, using a simple Bash script or a Fastlane lane to orchestrate the framework build and copy has proven reliable.

What to Share (and What Not to Share)

Strategic sharing is the key to KMP success. Sharing too much can create complexity; sharing too little misses the point.

Ideal Candidates for Shared Code

Start with the low-hanging fruit that offers the highest return on investment: Data Models (sealed classes, data classes), Business Logic (validation, calculations, rules engines), Networking (using Ktor's multiplatform client), Local Caching (using SQLDelight for multiplatform SQLite), and Utilities (date formatting, string manipulation, complex algorithms). Sharing these elements ensures absolute consistency in how your app's core functions behave, regardless of the platform.

The UI Layer: Keep It Native

Resist the temptation to abstract the UI. The UI should remain firmly in the native domain. The shared module exposes view models or state holders (using a library like `kotlinx.coroutines` for state flow) that the native UI observes and reacts to. This clean separation aligns perfectly with modern MVVM or MVI patterns.

Platform-Specific APIs: Using Expect/Actual Wisely

Use `expect`/`actual` for essential platform integrations. Common examples include cryptography (KeyStore vs. Keychain), biometric authentication, file system access for specific directories, and deep linking. Abstract these behind a clean, shared interface. Be judicious—if a piece of code is overwhelmingly different on each platform, it might not be a good sharing candidate.

Navigating Concurrency and Coroutines

Kotlin Coroutines are a cornerstone of modern Kotlin development and are fully supported in KMP, but their behavior differs slightly per target.

Coroutines in the Shared Module

You can use `kotlinx.coroutines` freely in your shared code for background operations, async flows, and state management. The shared module itself is agnostic of the dispatcher; it relies on the consuming platform to provide the main dispatcher for UI updates.

Bridging to iOS with Kotlin/Native Memory Model

This is a critical technical detail. Kotlin/Native (which compiles the iOS code) historically had a strict memory model that required careful management of object freezing and thread confinement. The new Kotlin/Native memory manager, enabled by default in Kotlin 1.7.20+, is a game-changer. It provides a fully automatic, concurrent garbage collector similar to the JVM, eliminating most of the complex rules. If you're starting today, ensure you're using the new memory manager—it makes working with coroutines and state sharing with iOS dramatically simpler and more intuitive.

Exposing Flows to Swift

You can expose a `StateFlow` or `SharedFlow` from your shared view model. For iOS to consume it, you typically create an adapter that converts the Kotlin Flow into something Swift can easily use, like a `AsyncThrowingStream` or by using a callback wrapper. Libraries like `SKIE` are emerging to greatly simplify this interop, making Kotlin Flows and suspend functions feel native in Swift.

Testing Your Shared Code Effectively

A major benefit of KMP is that you can write your unit tests once and run them on all targeted platforms, guaranteeing identical logic.

Common Tests for Business Logic

Place your core unit tests in the `commonTest` source set. These tests will execute on the JVM (for Android) and natively (for iOS). Use `kotlin.test` and `kotlinx.coroutines.test` libraries. This is where you verify all your business rules, data transformations, and use cases.

Platform-Specific Test Validation

For `expect`/`actual` functions, write small integration tests in the `androidTest` and `iosTest` source sets. These verify that the platform-specific implementations behave correctly and as expected by the common code. For iOS, you can run these tests from Xcode or via command line on a CI server.

Integration Testing Strategy

While the shared logic is tested in isolation, you still need integration tests in your native Android and iOS projects. These tests validate that the native UI correctly binds to and interprets the states and events from the shared module. This two-layer testing strategy provides comprehensive coverage.

Managing Dependencies and Versioning

A well-organized dependency graph is vital for maintainability.

Choosing Multiplatform Libraries

Prefer official or robust community-supported multiplatform libraries. The ecosystem is growing rapidly. Key staples include: Ktor (HTTP client), kotlinx.serialization (JSON), SQLDelight (database), Kotlinx DateTime (date/time), and Napier (logging). Always check the library's documentation for supported targets.

Handling Platform-Only Dependencies

Use Gradle's `sourceSets` to declare dependencies that only exist on one platform. For example, you might use `androidMain` dependencies for Android-specific utilities and `iosMain` dependencies for iOS helper libraries. The `expect`/`actual` mechanism is how you wrap their functionality.

Versioning and Publishing Your Shared Module

Treat your shared KMP module as an internal product. Use semantic versioning. Automate its publishing via CI/CD. This allows mobile app teams to depend on a stable, versioned artifact, enabling them to update independently and roll back if necessary. It fosters a healthy API contract between the shared logic team and the platform UI teams.

Real-World Challenges and Solutions

Adopting any new technology comes with hurdles. Being prepared is half the battle.

Debugging iOS Code from Android Studio

While you can't directly debug Swift code in Android Studio, you can debug the Kotlin code that is executing within the iOS simulator or device. The KMM plugin allows you to attach the debugger to the iOS process. For pure iOS issues, you'll still need Xcode. A robust logging strategy (using `expect`/`actual` for a logger) is indispensable.

Build Time and CI/CD Considerations

KMP builds, especially compiling the iOS frameworks, can add time to your build pipeline. Cache your Gradle builds aggressively. Consider building the iOS framework only when the shared module changes, not on every app commit. Structure your CI jobs to build and publish the shared module separately, then have the app builds simply consume the pre-built artifact.

Team Structure and Knowledge Sharing

KMP introduces a new dimension: shared code engineers need to understand both Android and iOS concepts. Encourage cross-platform learning. Consider forming a central "platform" team that maintains the shared module while feature teams consume it. Clear communication and documentation of the shared module's API are critical.

Practical Applications: Where KMP Shines

KMP isn't a silver bullet for every app, but it excels in specific, high-value scenarios.

1. The Data-Driven Business App: Imagine a financial services app with complex loan calculation engines, tax rule processors, and data validation logic. By implementing these in a KMP shared module, you ensure that a loan eligibility calculation yields the exact same result on iOS and Android, which is not just convenient but a regulatory necessity. The UI can be tailored to each platform's design guidelines while the core math remains a single source of truth.

2. The Multi-Platform Media Player: A podcast or music streaming app has complex logic: playlist management, playback queue shuffling algorithms, offline download management, and subscription entitlement checks. This is pure business logic. Sharing it via KMP means a user's "Up Next" queue is perfectly synchronized across their Android phone and iPad, with the native video player controls on each device.

3. The E-Commerce Powerhouse: Shopping cart logic, coupon validation rules, inventory checking, and checkout flow state management are ideal for KMP. You can ensure the same promo code applies the same discount on both platforms, and the cart total is always calculated identically. The product pages can use completely native UI components for the best image carousels and animations.

4. The Cross-Platform Game with Shared Engine Logic: While the rendering might use OpenGL/Metal or a game engine, the game's core logic—character stats, AI behavior, level progression rules, and score calculations—can be written once in Kotlin. This allows a small indie team to maintain a consistent game experience across mobile platforms without rewriting the game's heart.

5. Legacy App Modernization: For a company with a large, mature Android app (in Kotlin) and a newer iOS team playing catch-up, KMP offers a strategic path. The Android business logic can be incrementally extracted into a shared module and consumed by both apps. This accelerates iOS feature parity while modernizing the Android codebase into a cleaner architecture.

Common Questions & Answers

Q: Is Kotlin Multiplatform production-ready?
A> Yes, absolutely. JetBrains uses it in production for products like Space. Major companies like Netflix, Philips, and Cash App use it for critical features. The tooling has matured significantly, especially with the new Kotlin/Native memory manager.

Q: Do my iOS developers need to learn Kotlin?
A> They need a basic understanding to read and potentially debug the shared logic, but not to write it extensively. Their primary job remains writing Swift/UIKit/SwiftUI. They interact with the shared code through generated Swift-friendly APIs (or via adapter libraries).

Q: How does KMP affect app size?
A> There is an overhead as the Kotlin/Native runtime is bundled into your iOS framework. For typical business apps, this adds a few megabytes to the final IPA, which is generally acceptable. The trade-off is reduced code duplication and faster feature development.

Q: Can I use Compose Multiplatform for the UI?
A> You can, but it's a separate decision. Compose Multiplatform is a UI framework built *on top of* KMP. This guide focuses on the logic-sharing use case with native UI. Compose Multiplatform allows you to share UI declarative code for Android, iOS, and desktop, representing a different, more ambitious approach.

Q: What's the biggest risk when adopting KMP?
A> The main risk is architectural. Poorly designed shared modules with leaky platform abstractions can become a bottleneck. Start small, share only pure logic, establish clear API boundaries, and treat the shared module as a first-class product with its own tests and versioning.

Q: How is the community and library support?
A> The community is vibrant and growing quickly. Library support is no longer a major blocker for common tasks (networking, serialization, databases). For niche needs, you may need to write your own `expect`/`actual` wrappers, which is a straightforward process.

Conclusion: A Strategic Tool for Modern Teams

Kotlin Multiplatform is not a fantasy of 100% code sharing; it's a practical engineering tool for achieving 80-90% logic sharing while delivering 100% native user experiences. It addresses the real pain points of duplication, inconsistency, and slowed velocity that plague multi-platform teams. The journey requires an upfront investment in learning and setup, but the long-term payoff in consistency, maintainability, and team synergy is substantial. My recommendation is to start tactically: identify a single, complex domain logic component in your current apps, isolate it, and port it to a small KMP shared module. Measure the reduction in bugs and development time. Let that success guide your next steps. By embracing KMP's philosophy of shared logic and native UI, you can build better, more consistent apps faster, finally turning the cross-platform dilemma into a competitive advantage.

Share this article:

Comments (0)

No comments yet. Be the first to comment!