The Problem: Why Your Codebase Feels Like a Tower of Babel
Building mobile apps for both Android and iOS often means writing the same business logic twice—once in Kotlin, once in Swift. This duplication isn't just tedious; it's a breeding ground for bugs, inconsistencies, and wasted effort. Every time you add a feature, you must implement it in two languages, test it on two platforms, and fix any divergence. For teams with limited resources, this can double development time and increase maintenance overhead. The problem becomes even more painful when you need to share logic with a web frontend or backend. Each platform demands its own language and framework, forcing you to either maintain multiple codebases or settle for subpar cross-platform solutions that sacrifice native performance or user experience.
Consider a typical scenario: a social media app that needs to validate user input, format timestamps, and manage authentication tokens. On Android, you write these functions in Kotlin; on iOS, you rewrite them in Swift. Any change to the validation rules—say, a new password policy—must be applied in two places, often with subtle differences that lead to inconsistent behavior. Multiply this by dozens of features, and you have a maintenance nightmare. The core frustration is that the logic itself is platform-agnostic; only the UI and platform APIs differ. Yet traditional development forces you to duplicate the non-UI parts.
This is where Kotlin Multiplatform enters as a solution, allowing you to share code across platforms while keeping the native user interface. Think of it like a book exchange: you write the story once (the shared logic), and each friend reads it in their own language (the platform-specific UI). The story remains the same, but the reading experience adapts to each person's preferences. In technical terms, KMP compiles Kotlin code to platform-specific binaries—JVM bytecode for Android, native code for iOS, and JavaScript for web—so you can reuse logic without sacrificing performance or access to platform APIs.
The stakes are high: businesses that fail to streamline cross-platform development risk slower time-to-market, higher costs, and a fragmented user experience. By adopting KMP, you can reduce code duplication by up to 60% for common logic layers, according to many industry surveys. But the journey requires understanding how KMP works, what to share, and where to draw the line between shared and platform-specific code. In this guide, we'll walk you through the core concepts, practical workflows, and common pitfalls, all framed through the lens of a friendly book exchange.
So, if you're tired of rewriting the same logic for every platform, it's time to explore how KMP can help you share code like you share books—effortlessly and enjoyably. Let's begin by unpacking the core frameworks that make this possible.
Core Frameworks: How the Book Exchange Actually Works
Kotlin Multiplatform isn't a single tool but a collection of components that work together to enable code sharing. At the heart is the Kotlin compiler, which can produce multiple targets from the same source code. You define a shared module—typically called 'shared' or 'common'—where you place all platform-independent logic: data models, business rules, networking, and algorithms. This module is then compiled for each target you specify, such as Android (JVM), iOS (native via Kotlin/Native), and web (JavaScript via Kotlin/JS). The beauty is that you write the code once in Kotlin, and the compiler handles the rest.
The key mechanism is the 'expect/actual' pattern. When you need platform-specific functionality—like accessing the file system, making a network request with platform-specific APIs, or generating a UUID—you declare an 'expect' function or class in the common module. Then, for each target, you provide an 'actual' implementation. For example, you might define an expect function `fun getPlatformName(): String` in common, and then in the Android source set, provide `actual fun getPlatformName(): String = "Android"`; similarly for iOS, return "iOS". This pattern ensures that the common code remains clean and platform-agnostic, while the actual implementations can use platform SDKs directly.
In our book-exchange analogy, the shared module is the library of books you want to share. The expect/actual declarations are like instructions for translating the story into different languages: the story remains the same, but the translation adapts to each reader's language. The compiler acts as the translator, producing a version for each friend. For Android, the output is a JAR file that runs on the JVM; for iOS, it's a framework that can be imported into an Xcode project; for web, it's a JavaScript module. You don't need to worry about the translation details—just trust the compiler.
Another critical component is the Kotlin Multiplatform plugin for Gradle, which manages dependencies and source sets. You define source sets like `commonMain`, `androidMain`, `iosMain`, and so on. Libraries that are platform-agnostic, such as kotlinx.serialization for JSON parsing or kotlinx.coroutines for async operations, can be added to `commonMain`. Platform-specific dependencies, like OkHttp for Android or NSURLSession for iOS, go into the respective source sets. The plugin ensures that only the correct libraries are used when compiling for a specific target.
For beginners, the most important framework to understand is the shared module structure. You'll typically have a Gradle module named 'shared' with a `src` directory containing `commonMain`, `androidMain`, `iosMain`, and sometimes `jsMain`. Inside `commonMain`, you write the bulk of your logic. The platform-specific directories hold only the actual implementations for expect declarations. This separation is crucial for maintaining a clean architecture.
Many teams initially try to share too much, including UI code. KMP is not designed for sharing UI—at least not directly. Instead, you share the underlying logic and data, and then build native UI on each platform that consumes the shared module. This approach gives you the best of both worlds: code reuse for business logic and native UI for optimal user experience. Think of it as writing a book's plot and characters (shared logic) and letting each publisher design the cover and illustrations (platform UI).
In summary, the core frameworks—Kotlin compiler, expect/actual, Gradle plugin, and source sets—form the foundation of your book exchange. With these, you can write once and run on multiple platforms, reducing duplication and simplifying maintenance. Next, we'll walk through a repeatable process for setting up your first shared module.
Execution: A Step-by-Step Workflow for Your First Shared Module
Let's turn theory into practice. Setting up a Kotlin Multiplatform project involves several concrete steps. We'll use a composite scenario: a note-taking app that needs to save notes locally and sync them to a server. The shared logic includes note models, validation, and networking, while the UI will be built natively on each platform.
First, create a new project in Android Studio with the KMP template. If you don't have the template, you can manually create a Gradle project with an empty `settings.gradle.kts` file. Add the Kotlin Multiplatform plugin and specify targets. For example: `plugins { kotlin("multiplatform") version "1.9.22" }`. Then, configure targets: `kotlin { androidTarget(); iosX64(); iosArm64(); iosSimulatorArm64(); jvm() }`. The JVM target is useful for backend or desktop, but we'll focus on Android and iOS.
Next, define the source sets. In your `build.gradle.kts` for the shared module, add `sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") } }; val androidMain by getting; val iosMain by creating { dependsOn(commonMain) } }`. The `iosMain` source set is created manually because iOS targets share it. For each platform-specific source set, you can add dependencies like `implementation("io.ktor:ktor-client-android:2.3.7")` for Android networking.
Now, write your shared code. Create a data class `Note` in `commonMain`: `data class Note(val id: String, val title: String, val content: String, val timestamp: Long)`. Add a `NoteRepository` interface with methods like `suspend fun getNotes(): List` and `suspend fun saveNote(note: Note)`. Then, provide platform-specific implementations using expect/actual. For example, expect a `PlatformDatabase` class that provides a `getDatabase()` function. On Android, actual implementation uses Room; on iOS, it uses CoreData or a SQLite wrapper. This pattern keeps the repository interface clean and testable.
For networking, use Ktor client, which works on both platforms. In `commonMain`, write a `NoteApi` class that makes HTTP requests. Ktor handles the underlying engine: on Android, it uses OkHttp; on iOS, it uses NSURLSession. You just need to add the appropriate engine dependency in each source set. This is a great example of how KMP abstracts platform differences.
Testing is another area where sharing shines. Write unit tests in `commonTest` that test the repository and API logic using mocks or in-memory databases. These tests run on the JVM during development, and you can also run them on iOS simulators if needed. This reduces the need for separate test suites for each platform.
Finally, integrate the shared module into your Android and iOS apps. For Android, add the shared module as a dependency in your app's `build.gradle.kts`. For iOS, the KMP plugin generates a framework; you need to add it to your Xcode project using an embed framework build phase. In Xcode, you import the framework and call shared functions from Swift or Objective-C. The API is automatically bridged—Kotlin's coroutines become Swift async functions if you use the Kotlin-Swift interop.
Throughout this process, keep the shared module focused on logic only. Resist the temptation to put UI elements in shared code. Instead, create platform-specific view models that use the shared repository and expose state to the native UI. This separation keeps your codebase maintainable and your teams happy. With this workflow, you can go from zero to a working shared module in a couple of days. Next, we'll examine the tools, stack, and economics that make KMP sustainable in the long term.
Tools, Stack, and Economics: What You Need to Sustain the Exchange
Adopting KMP requires not just understanding the code but also investing in the right tooling and being realistic about costs. On the tooling front, Android Studio is the primary IDE for KMP development, with excellent support for Kotlin and Gradle. You'll also need Xcode for iOS-specific work, but you can write most shared code from Android Studio. The Kotlin Multiplatform plugin provides a unified build system, but you may need to manually configure Xcode integration for iOS targets.
Your technology stack should include libraries that are KMP-compatible. For networking, Ktor is the go-to choice because it's designed for multiplatform from the ground up. For serialization, kotlinx.serialization works seamlessly. For local storage, you have options: SQLDelight generates type-safe Kotlin APIs for SQLite and supports multiplatform, while Realm Kotlin SDK also offers multiplatform support. For dependency injection, Koin or Kodein have KMP support. Avoid libraries that have limited multiplatform support, as they may force you into platform-specific workarounds.
Testing tools are equally important. The Kotlin test framework works across common tests, and you can use MockK for mocking in shared code. For UI testing, you'll still need platform-specific frameworks like Espresso (Android) and XCTest (iOS), but the shared logic tests reduce overall test effort. Many teams report a 30-40% reduction in total test code when using KMP, as they don't need to duplicate logic tests.
Economics and maintenance realities: The initial setup cost is higher than a single-platform app. You need to learn KMP concepts, configure the Gradle build, and set up Xcode integration. For a team of two developers, this might add 1-2 weeks to the initial project timeline. However, over the life of the project, the savings from reduced duplication can offset this. For example, adding a new feature that involves business logic might take 3 days in a dual-codebase setup, but only 2 days with KMP (assuming the shared module covers the logic). Over a year with 20 features, that's 20 days saved—almost a month of developer time. Additionally, bug fixes in shared code propagate to all platforms instantly, eliminating the risk of patches being missed on one platform.
Maintenance costs also shift. You'll need to keep up with KMP updates, which are frequent but generally backward-compatible. The community is active, and most issues are documented. One hidden cost is the need for platform-specific experts: even with shared logic, you still need developers who know Android and iOS UI frameworks. KMP doesn't eliminate that; it just reduces the amount of platform-specific code. In addition, you may need to invest in CI/CD that can build for multiple targets, which might require additional hardware (macOS for iOS builds).
Despite these costs, for many projects, KMP is a net positive. The key is to start small—share only the most stable logic first (models, networking, validation) and gradually expand as the team gains confidence. Avoid sharing code that changes frequently (like UI state management) until you have a solid pattern. Next, we'll explore how to grow your KMP usage and position your team for long-term success.
Growth Mechanics: Scaling Your Book Exchange Across the Team
Once you have a working shared module, the next challenge is scaling its usage across your team and project. Growth mechanics involve both technical and organizational factors. Technically, you need to design the shared module to be extensible. Use modular architecture: split the shared code into smaller libraries (e.g., `shared-core`, `shared-network`, `shared-database`). This allows teams to depend on only what they need and reduces compilation times. For example, the iOS team might only need `shared-core`, while the Android team also uses `shared-ui-models` (which are still logic, not UI).
Another growth strategy is to adopt a layered approach. Start with a thin shared layer that handles only data models and simple utilities. As you gain confidence, add networking and database layers. Eventually, you might share business logic like form validation, permission handling, or even navigation state (but not navigation UI). This incremental approach reduces risk and allows the team to learn gradually.
Positioning KMP within a larger team requires buy-in from all developers. For iOS developers, the idea of writing logic in Kotlin can be off-putting. However, once they see that they can focus on beautiful SwiftUI screens while the messy business logic is handled by shared code, they often become advocates. One team I read about started with only two developers—one Android and one iOS—who paired on the shared module. After three months, they had a shared networking and database layer that saved them 15% development time. The rest of the team was then convinced to adopt KMP for new features.
Documentation and coding standards are crucial. Establish clear guidelines for what goes into shared code and what stays platform-specific. For example, any code that calls a platform API (like `NSNotificationCenter` or `Android SharedPreferences`) must be behind an expect/actual declaration. Create a decision tree: if the code uses a platform SDK, it's platform-specific; if it's pure logic, it's shared. Use code reviews to enforce this discipline.
Another growth enabler is to build a common library of shared utilities—like date formatting, string validation, and encryption—that multiple teams can reuse. This library becomes the "book exchange" hub, where each team contributes and benefits. Over time, the shared library grows, and new projects can start with a significant amount of pre-built logic.
Finally, measure success. Track metrics like percentage of code shared (e.g., lines of shared code vs. platform-specific code), time to implement new features, and frequency of cross-platform bugs. Share these metrics with stakeholders to justify continued investment. Many teams aim for 30-50% shared code after the first year. Remember, growth is not just about sharing more code; it's about sharing the right code—logic that is stable, testable, and truly cross-platform. Next, we'll look at common pitfalls and how to avoid them.
Risks, Pitfalls, and Mistakes: What Can Go Wrong in the Exchange
Even with careful planning, KMP projects can hit snags. The most common pitfall is over-sharing—putting UI-related code or platform-specific dependencies in the common module. For example, a developer might add an Android-specific library to `commonMain` because it's convenient, breaking the iOS build. The fix is strict adherence to source set dependency rules: only KMP-compatible libraries go in `commonMain`. Use the expect/actual pattern for any platform-specific call.
Another frequent mistake is ignoring concurrency differences. Kotlin coroutines are single-threaded by default on iOS, which can lead to unexpected behavior if you assume multithreading. For instance, you might use `withContext(Dispatchers.IO)` in common code, but on iOS, the actual dispatcher might be single-threaded, causing blocking. The mitigation is to test coroutine behavior on all target platforms early. Use `Dispatchers.Default` for CPU-bound work and `Dispatchers.IO` for I/O, but be aware that on iOS, these are implemented differently. You can also create a custom dispatcher that uses `NIO` on iOS.
Dependency management can also trip up newcomers. Some libraries claim multiplatform support but have incomplete iOS implementations. Always check the library's documentation and test it on all targets before committing. For example, a popular networking library might work on Android but fail on iOS due to missing native engine. Use Ktor or a well-maintained alternative like Fuel (which has KMP support). If a library doesn't support iOS, you may need to wrap it in an expect/actual interface and provide a separate implementation for iOS, which defeats the purpose of sharing.
Another risk is performance issues due to object allocation and memory management. Kotlin/Native uses reference counting, which can cause memory leaks if you have circular references in shared code. Use `WeakReference` or redesign data structures to avoid cycles. Also, be mindful of large data transfers across the Kotlin-Swift bridge: passing large lists or images can be slow. Instead, pass data in smaller chunks or use platform-specific APIs for heavy lifting.
Build times can also become a pain point. Compiling for multiple targets takes longer than single-platform builds. Use Gradle build caching and parallel execution. Consider using a remote build cache. Also, modularize the shared code so that changes in one module don't require rebuilding everything. For example, split networking and database into separate Gradle modules.
Finally, team resistance is a real non-technical risk. Some developers prefer native development and may resist KMP. Address this by involving them early in decision-making, showing them the benefits, and allowing them to contribute to the shared module's design. Provide training and pair programming sessions. Acknowledge that KMP is not a silver bullet—it's a tool for specific problems. If the team is small and the app is simple, native development might be faster. Evaluate the trade-offs honestly and adjust based on feedback. By anticipating these pitfalls, you can navigate your KMP journey more smoothly. Next, we'll answer common questions to solidify your understanding.
Mini-FAQ: Common Questions About Sharing Code Like Books
This section addresses frequent questions from developers evaluating KMP. We've compiled them from forums, community discussions, and our own experience with teams adopting KMP.
1. Is KMP production-ready for iOS?
Yes, KMP is production-ready for iOS as of Kotlin 1.9.x and beyond. Many apps in the App Store use KMP, including Netflix, McDonald's, and VMWare. However, you may encounter issues with third-party library support. Always test early.
2. How much code can I realistically share?
For a typical mobile app, you can share 40-60% of the code, mainly business logic, networking, data models, and validation. UI code remains platform-specific. Some teams share up to 80% for apps with complex logic but minimal UI.
3. Does KMP replace React Native or Flutter?
No. KMP is a different approach: it shares logic while keeping native UI. React Native and Flutter share UI as well. Choose KMP if you want native performance and UI, and are willing to invest in platform-specific UI development. Choose Flutter if you want a single UI codebase.
4. Can I use Swift libraries from KMP?
Indirectly. KMP can call Swift code through the Objective-C bridge, but it's not seamless. You can create an expect/actual interface for Swift-specific functionality, but it's easier to keep platform-specific code in native files.
5. How do I handle dependencies that don't support KMP?
You have two options: (a) wrap the dependency behind an expect/actual interface, providing separate implementations per platform; (b) avoid the dependency and find a KMP-compatible alternative. The latter is preferred to keep code sharing high.
6. What's the learning curve for a Swift developer?
Kotlin syntax is similar to Swift, so Swift developers can pick it up in a week. The main challenge is understanding the build system and expect/actual pattern. Pair programming with a Kotlin developer helps.
7. Can I share code with a backend written in Kotlin?
Yes! You can create a JVM target for your shared module and use it in a Ktor backend. This enables code reuse between mobile and server, such as data models and validation logic.
8. How does testing work with KMP?
Write unit tests in `commonTest` that run on both platforms. Use a test framework like kotlin.test. For platform-specific tests, write them in `androidTest` or `iosTest`. You can run common tests on the JVM during development for fast feedback.
9. What about continuous integration?
Use a CI service that supports macOS agents (for iOS builds) and Linux/Windows agents (for Android). GitHub Actions, Bitrise, and CircleCI all have workflows for KMP. Cache Gradle dependencies to speed up builds.
10. Should I start with KMP for a new project?
If you need to target both Android and iOS from the start, and you have a team with Kotlin and Swift skills, yes. If you're prototyping or have a small team, consider starting with a native app and add KMP later. The decision depends on your project's complexity and timeline.
These answers should clarify common doubts. If you have more questions, the KMP community on Slack and Reddit is very active. Now, let's synthesize everything into actionable next steps.
Synthesis and Next Actions: Start Your Own Book Exchange
We've covered a lot of ground. Kotlin Multiplatform offers a compelling way to share business logic across platforms, reducing duplication and maintenance costs. The book-exchange analogy—writing the story once and letting each friend read it in their native language—captures the essence: shared logic, native UI. To begin your own journey, here are concrete next actions.
Week 1: Set up a small proof-of-concept project. Create a shared module with a simple data model and a function that returns a formatted date string. Build and test it on both Android and iOS. This will familiarize you with the toolchain and build process. Use the official Kotlin Multiplatform wizard to generate a project template.
Week 2: Expand the shared module to include networking using Ktor and serialization with kotlinx.serialization. Write a simple API call that fetches data and displays it in a list on each platform. This will test your expect/actual pattern for logging or error handling.
Week 3: Add a local database using SQLDelight. Create a repository that combines networking and local storage. Write tests for the repository in `commonTest`. At this point, you'll have a solid foundation for a real app feature.
Week 4: Integrate the shared module into your existing app (or a new feature). Start with a non-critical feature like a settings screen or a help page. Monitor the build times and team feedback. Adjust your shared code architecture based on lessons learned.
Beyond: Gradually move more logic into the shared module. Consider creating a shared UI-components library using Compose Multiplatform if you want to share UI as well, but be aware that it's still evolving. Document your patterns and share them with the team. Contribute back to the KMP community by writing a blog post or answering questions.
Remember, KMP is not a magic bullet. It requires discipline in separating concerns and a willingness to learn new tools. But for many teams, the payoff in reduced duplication, faster feature development, and consistent user experience is well worth the effort. Start small, iterate, and soon you'll be sharing code like you share books—naturally and efficiently.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!