Why Sharing Code Between Android and iOS Feels Like Building Two Separate Libraries
Imagine you are a librarian tasked with building two libraries: one for Android users and one for iOS users. Each library must have its own unique architecture, but they share the same books, the same cataloging system, and the same borrowing rules. Historically, developers have built these two libraries from scratch, duplicating the cataloging logic, the book metadata, and the membership management code. This is exactly what happens when you write separate Android and iOS apps: you duplicate business logic, data models, networking code, and validation rules. Kotlin Multiplatform (KMP) offers a way to build a shared "library section" once and reuse it across both platforms, while still allowing each platform to have its own unique reading experience. In this guide, we will map KMP modules to library sections, making the concept intuitive for beginners.
The Pain of Duplicate Code
Consider a typical mobile app that fetches user profiles from a server. On Android, you write a repository class, a network client, and a data model. On iOS, you rewrite the same logic in Swift or Objective-C. Every time the API changes, you update two codebases. This duplication leads to bugs, slower development, and higher maintenance costs. According to many industry surveys, teams can spend up to 30% of their time syncing changes between platforms. KMP addresses this by allowing you to write the networking, data parsing, and business logic once in Kotlin, then compile it to both Android bytecode and iOS native binaries. The shared module becomes your library's central catalog—consistent, tested, and maintained in one place.
How the Library Analogy Works
Think of your app as a library building. The shared KMP module is the reference section: it contains the core knowledge (business logic) that both the Android reading room and the iOS reading room need. The Android app can borrow from this reference section using Kotlin/JVM, while the iOS app accesses the same section via Kotlin/Native, which compiles to a framework that Swift can call. The UI layer, however, remains platform-specific—like the interior design of each reading room. You wouldn't force a cozy iOS armchair onto Android's minimalist shelves. This separation of concerns is key: share what is common, customize what is not.
By the end of this guide, you will have a clear mental model for structuring KMP modules, know the steps to set up your first shared project, and understand how to avoid common mistakes. Let's start by exploring the core frameworks that make this possible.
Core Frameworks: How Kotlin Multiplatform Works Under the Hood
To understand KMP, you must first grasp three key concepts: the shared module, expect/actual declarations, and the Kotlin compiler's ability to target multiple platforms. Think of the shared module as the library's reference section—a collection of pure Kotlin code that contains business logic, data models, and network clients. This module has no dependencies on Android SDK or iOS UIKit; it stays platform-agnostic. When you compile, the Kotlin compiler produces a JAR or Android library for Android, and a native framework (via LLVM) for iOS. This is analogous to printing the same reference book in two different formats: paperback for one library and a digital e-reader for another. The content is identical, but the medium adapts.
Expect/Actual: The Bridge Between Shared and Platform Code
Sometimes, your shared logic needs to perform platform-specific operations, like reading a file or generating a UUID. KMP handles this with expect/actual declarations. In the shared module, you declare an expect function or class—a placeholder that says, "This exists, but each platform will provide its own implementation." Then, in platform-specific source sets (like androidMain and iosMain), you provide the actual implementation. For example, you might have an expect fun generateUUID(): String in shared code, with actual implementations using java.util.UUID on Android and NSUUID on iOS. This pattern is like having a library policy that says, "We will have a certain service, but each branch can decide how to execute it." The shared code is not polluted with platform specifics, yet it can leverage native APIs when needed.
The Role of Kotlin/Native and Kotlin/JVM
KMP compiles shared code to two main targets: Kotlin/JVM for Android (running on the Java Virtual Machine) and Kotlin/Native for iOS (compiling to native machine code via LLVM). Kotlin/Native produces a dynamic framework (e.g., shared.framework) that you can embed in an Xcode project. The framework exposes Kotlin classes and functions to Swift or Objective-C, with automatic mapping of basic types (like String to NSString). However, more complex types like sealed classes or coroutines require some bridging. The Kotlin compiler handles this by generating Objective-C headers that Swift can consume. This process is akin to translating a book's table of contents into a language the local readers understand—the structure remains, but the presentation adapts.
Understanding these mechanisms helps you design modules that are truly shareable. Next, we will walk through the actual workflow of setting up a KMP project.
Setting Up Your First Shared Module: A Step-by-Step Workflow
Creating a KMP project from scratch can feel overwhelming, but by following a repeatable workflow, you can set up a shared module in under an hour. We'll use the official Kotlin Multiplatform wizard or manual Gradle configuration. The goal is to produce a module that contains business logic and data models, while leaving UI code to platform-specific projects. Think of this as designing the reference section of your library before building the reading rooms.
Step 1: Generate the Project Structure
Start by visiting the Kotlin Multiplatform Wizard (play.kotlinlang.org) or use IntelliJ IDEA's KMP plugin. Select targets: Android (JVM) and iOS (ARM64 for devices, X64 for simulators). The wizard creates a project with three source sets: commonMain (shared code), androidMain, and iosMain. This is like having a central archive (common) and two branch archives. In commonMain, you'll write your core logic—data models, repository interfaces, and use cases. For example, define a User data class with fields like id, name, and email. This class will be available on both platforms without modification.
Step 2: Add Dependencies for Common Networking
To fetch data from a REST API, you need a networking library that works in common code. Ktor is a popular choice because it's written in Kotlin and supports both JVM and Native. Add io.ktor:ktor-client-core to commonMain dependencies, and platform-specific engines (ktor-client-okhttp for Android, ktor-client-darwin for iOS) to their respective source sets. This is like ordering books for your library: the core cataloging system (Ktor client) is the same, but the delivery trucks (engines) differ by location. Then, write a UserRepository class in common that uses Ktor to make HTTP calls and parse JSON with kotlinx.serialization. Test this class on both platforms using unit tests in commonTest.
Step 3: Integrate with Platform UI
On Android, add the shared module as a dependency in your app's build.gradle.kts. You can then call UserRepository from a ViewModel. On iOS, you need to build the shared framework (using a Gradle task like embedAndSignAppleFrameworkForXcode) and embed it in Xcode. In Swift, you import the framework and call Kotlin classes directly, with some bridging for more complex types. For instance, a Kotlin suspend function is exposed as a callback-based function in Swift. This step is like placing the reference books on the shelves of each reading room—the books are the same, but the shelving (integration) is done per room.
By following this workflow, you create a repeatable process for adding shared logic. Next, we'll explore the tools and economics that make KMP a viable choice for teams.
Tools, Stack, and Maintenance Realities of KMP Projects
Adopting KMP involves selecting the right tools and understanding the ongoing maintenance costs. The ecosystem has matured significantly, but it still requires careful configuration. Think of this as equipping your library with the right software for cataloging, borrowing, and security. The core toolchain includes IntelliJ IDEA, the Kotlin Multiplatform plugin, Gradle, and Xcode. For CI/CD, you'll need to build the shared framework for iOS separately, as it requires macOS runners. Many teams use GitHub Actions or Bitrise with macOS agents.
Comparison of Networking Libraries
| Library | Common Code Support | iOS Integration | Learning Curve |
|---|---|---|---|
| Ktor | Full | Darwin engine | Moderate |
| Retrofit (via KMP wrapper) | Partial (needs expect/actual) | Manual setup | High |
| Apollo GraphQL | Full | Native framework | Moderate |
Ktor is the most straightforward because it's built for multiplatform from the ground up. Retrofit, while popular on Android, requires extra effort to bridge to iOS. Apollo is excellent if your API is GraphQL, but adds complexity. Choose based on your team's familiarity and API style.
Economics and Maintenance
Sharing code reduces duplication, but introduces new costs: build time increases (you compile both targets), and debugging across platforms can be trickier. Many teams report that the initial investment of setting up KMP pays off after the second or third shared feature, as changes propagate automatically. However, you must maintain the shared module's dependencies—upgrading Ktor or kotlinx.serialization affects both platforms. This is like updating your library's catalog system: the change is global, so you need thorough testing. Consider using a monorepo or submodules to keep the shared code and platform projects in sync. Also, note that some libraries (like those for UI or platform-specific sensors) are not shareable; keep them in platform projects.
Understanding these trade-offs helps you make informed decisions. Next, we'll look at how sharing code can grow your app's features and team productivity over time.
Growth Mechanics: How Shared Modules Scale Your Development
One of the most compelling benefits of KMP is how it scales with your project. As your app grows, the shared module becomes a single source of truth for business rules, data transformations, and network requests. This is like a library that expands its reference section: every new book added benefits both reading rooms simultaneously. In practice, teams can add new features faster because they write the logic once. For example, if you add a new API endpoint for user settings, you update the repository in common code, and both Android and iOS get the change immediately after the next build.
Compound Benefit Over Time
Consider a scenario where you maintain three features: user authentication, product catalog, and push notification handling. Without KMP, each feature requires two implementations. With KMP, you write the authentication flow (including token storage, refresh logic, and session management) once in common code, using expect/actual for platform-specific secure storage (e.g., EncryptedSharedPreferences on Android, Keychain on iOS). Over a year, as you add more features, the shared codebase grows linearly while the platform-specific code stays mostly UI-focused. Many teams report a 30-50% reduction in total lines of code after the first six months, leading to fewer bugs and easier onboarding of new developers.
Team Productivity and Knowledge Sharing
With a shared Kotlin codebase, Android and iOS developers can collaborate more easily. They can review the same pull requests, write shared unit tests, and discuss business logic in a unified language. This reduces the "two-team" silo effect where each platform team diverges in implementation details. However, it requires that iOS developers are comfortable reading Kotlin. Many teams mitigate this by providing training or pairing sessions. The shared module also becomes a reusable asset for future apps—if you build a new app that uses the same backend, you can reuse the entire shared module. This is like a library system that shares its reference section across multiple branches, saving each branch from rebuilding the catalog.
Scaling also means handling dependencies wisely. Use version catalogs in Gradle to manage shared library versions, and automate the iOS framework build with a Gradle task integrated into Xcode's build phases. This ensures consistency. Next, we will examine the pitfalls that can derail your KMP journey.
Risks, Pitfalls, and Mistakes: What to Avoid When Starting KMP
While KMP is powerful, it comes with its own set of challenges. Beginners often fall into traps that can lead to frustration or project failure. The most common mistake is trying to share too much code, especially UI code. KMP is not designed for cross-platform UI like Flutter or React Native; its strength is in sharing logic. Attempting to share UI via Compose Multiplatform is possible but adds complexity and may not fit all design requirements. Stick to sharing business logic, data models, and networking, and keep UI code in native SwiftUI or Jetpack Compose.
Pitfall 1: Overusing Expect/Actual
Another frequent pitfall is using expect/actual for every small platform difference. This can lead to a fragmented codebase where you have dozens of expect declarations, each with two actual implementations. Instead, try to design your shared code to minimize platform dependencies. For example, instead of having an expect for file reading, pass a platform-provided data source to your shared logic. This keeps the shared module pure and testable. A good rule of thumb is to use expect/actual only for APIs that are truly platform-specific, like secure storage or camera access.
Pitfall 2: Ignoring Build Configuration
Build configuration is another area where mistakes happen. The iOS framework must be built on macOS, and if your CI uses Linux, you cannot produce iOS binaries. You'll need a macOS runner or a service like Mac Stadium. Also, forgetting to update the Xcode project's framework search paths can cause linker errors. A typical workflow is to run a Gradle task (e.g., ./gradlew :shared:embedAndSignAppleFrameworkForXcode) from a build phase script in Xcode. Automate this to avoid manual steps.
Pitfall 3: Underestimating Third-Party Library Maturity
Not all Kotlin libraries are multiplatform-ready. Check the library's documentation for KMP support before adding it. Libraries like Ktor, kotlinx.serialization, and SQLDelight have first-class support, while others may require workarounds. If a library lacks KMP support, you may need to wrap it with expect/actual, which increases maintenance. Always prefer libraries that explicitly list KMP targets. Also, be aware that some Android-specific APIs (like those in android.*) are not available on iOS. Use the Kotlin standard library and multiplatform libraries as much as possible.
By being aware of these pitfalls, you can navigate KMP adoption more smoothly. Next, we'll answer common questions that beginners have.
Frequently Asked Questions: A Decision Checklist for KMP Beginners
When starting with KMP, developers often ask the same questions. This mini-FAQ addresses the most common concerns and provides a quick decision checklist to help you evaluate if KMP is right for your project. Think of this as a library guidebook that answers, "Which books should I borrow?" and "How do I return them?"
Q1: Should I use KMP for a new project or migrate an existing one?
For new projects, KMP is easier to adopt because you can design the shared module from the start. For existing apps, consider migrating incrementally: extract networking and data models into a shared module first, then move business logic. This incremental approach reduces risk. A good candidate is an app with a stable backend API and clear separation of concerns.
Q2: How do I handle platform-specific UI patterns?
Keep UI code entirely in platform projects. Use the shared module only for the data and logic that drives the UI. For example, a ViewModel on Android and a ViewModel on iOS can both depend on the same UserRepository from shared code. The UI layer then observes the data and renders it natively. This respects each platform's design guidelines.
Q3: What about testing?
Write unit tests in commonTest for shared logic. These tests run on both JVM and Native, ensuring correctness across platforms. For platform-specific tests, write them in the respective source sets. Use a testing framework like kotlin.test, which works on all targets. This reduces duplication in test code as well.
Decision Checklist
- Shared logic exists? If your app has significant business logic, data models, or networking, KMP is beneficial.
- Team skills align? Both Android and iOS teams should be willing to learn Kotlin basics.
- CI/CD supports macOS? You need macOS runners to build the iOS framework.
- Library ecosystem matches? Verify that your required libraries have KMP support.
- Incremental adoption possible? Can you start with a small module and expand?
If you answered yes to most, KMP is a solid choice. Otherwise, consider alternative cross-platform solutions. Now, let's synthesize everything into actionable next steps.
Synthesis and Next Actions: Your Roadmap to KMP Adoption
We have mapped Kotlin Multiplatform modules to library sections, explored how they work, walked through setup, discussed tools and economics, examined growth mechanics, and identified pitfalls. Now it's time to take action. Your next steps are straightforward: start small, focus on sharing logic, and iterate. The library analogy should guide your design: keep the reference section (shared module) pure and well-organized, and let each reading room (platform UI) express its unique character.
Immediate Steps to Begin
- Set up a hello-world KMP project using the official wizard. Create a shared module with a simple data class and a function that returns a greeting.
- Integrate the shared module into both Android and iOS apps. On iOS, build the framework and call the greeting function from Swift.
- Add a network call using Ktor and kotlinx.serialization. Fetch a public API (e.g., a list of books) and display the results natively on each platform.
- Write unit tests for the shared logic in commonTest. Verify that parsing and business rules work identically on both platforms.
- Iterate by moving an existing feature (like user login) into the shared module. Use expect/actual for secure token storage.
As you progress, remember that KMP is a tool, not a silver bullet. It excels at reducing duplication in business logic but requires investment in build infrastructure and team learning. Many teams have successfully adopted KMP and report higher productivity and fewer bugs. The key is to start with a clear scope and expand gradually. The library section of your app's codebase will grow organically, serving both platforms efficiently.
Finally, stay updated with the Kotlin Multiplatform community. The ecosystem evolves rapidly, with new libraries and tooling improvements appearing regularly. By keeping your shared module clean and your platform-specific code focused on UX, you'll build a maintainable, scalable cross-platform application.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!