The main benefit of this migration? Migrating from native to multiplatform primarily aims to reduce costs and lead time for delivering new features. Now, why choose Kotlin Multiplatform (KMP) over Flutter or React Native? Because it can be done incrementally, migrating one application layer or feature at a time, which helps limit risks and easing integration.
I have personally taken part in such migrations. In this first article, I’ll explain the methodology followed.
🚀 In a second article, I’ll share a case study based on the practical application of this methodology.
Before starting the migration, it’s crucial to identify the differences between the iOS and Android versions of your application. The KMP migration provides an opportunity to unify some of these behavioral differences if it makes sense.
➡️ Why is this step essential?
It prevents creating a KMP implementation that is too Android-specific and ensures that the advantages of the iOS version are not lost.
There are also implementation differences, which is normal since iOS and Android have their own specifics. However, for architectural differences, take note of them: this will help you estimate the complexity of layer-by-layer migration. The more different the architectural layers are, the more complex the layer-based migration will be for iOS.
Before starting, some updates are necessary:
➡️ The goal is to ensure a smooth transition without unexpected technical roadblocks.
There are several ways to structure a multiplatform project; they are detailed on the KMP website.
I recommend the following approach, as it simplifies maintaining the migrated project:
This solution has its limitations. To go further, check out this article from TouchLab (a leader in KMP) discussing their tool Gitportal.
The migration should be done incrementally. Start with an isolated feature, one that is not central—a leaf or peripheral feature—to minimize risks.
❓How to find a leaf feature?
🔹 Why start with the model layer?
This layer usually has the fewest dependencies on external libraries and the least platform-specific code.
🟦 How to approach the model layer?
java.util.UUID
with kotlin.uuid.Uuid
).model
module for your feature by duplicating Android classes while accounting for potential iOS differences.The model layer is the easiest to migrate because it has few (if any) external dependencies. It represents the core of your application and generally has the fewest cross-platform differences.model
classes with imports from the shared module.After migrating the model layer, it might be tempting to move on to other layers. However, those layers often rely on cross-cutting modules. For example, your data layer might need the module that creates the HTTP client. You will also need your logger, analytics, utility functions, and even your design system at some point.
Thus, migrating these cross-cutting modules first is beneficial. However, it may be difficult to determine whether you need to migrate an entire module or only parts related to the feature being migrated.
It’s recommended to continue the migration steps while revisiting the cross-cutting modules as needed.
The Data layer handles network calls, database access, and file manipulations. It is closely tied to business logic. Migrate each part of this layer one by one.
If you are already using coroutines or Kotlin Flow instead of Rx or LiveData, the migration will be easier.
If your data interacts with other features, you have three options:
The business logic layer contains the rules specific to your application. This layer, which is close to the model layer, often includes functions that fetch, update, or delete data. It may heavily rely on external libraries such as Rx or LiveData to handle reactive data.
It communicates with the Data layer to retrieve and transform data.
To migrate this layer, move your reactive code adapters so they are used in the presentation layer instead of the business logic layer. This will align your business logic layer with Kotlin’s reactivity libraries (coroutines, flow).
This layer, responsible for feeding data to views via business logic, is often the least well-structured. Since the core of a mobile app revolves around displaying data and handling user interactions, this is where most of the code resides. Additionally, because it interacts with views, this layer frequently contains platform-specific code.
Be extra cautious here—this is where regressions are most likely to occur. Enhance your unit tests if needed.
🎯 Recommended Strategy
If you want to share the UI across platforms using Compose Multiplatform, now is the time to do so. Duplicate the Android views by re-implementing platform-specific elements in Compose for Android. Since Compose Multiplatform is very similar to Compose for Android, the required changes will be minimal. Once all Android-specific elements are removed, your Compose code will be usable on iOS.
By migrating views as well, you will significantly reduce communication between KMP code and Swift code. While KMP facilitates cross-platform communication, avoiding unnecessary platform-specific interactions is always preferable.
At this stage, most of the work is done. The remaining platform-specific elements, such as navigation and some native integrations, should be migrated progressively and carefully.
In this progressive strategy, the common code is designed to accommodate iOS, allowing iOS app development to continue without disruption while ensuring that the shared API is well-suited for the platform.
The more different the platform-specific code is in terms of architecture, components, naming conventions, behaviors, and logic, the less beneficial a gradual iOS migration becomes. In such cases, a less incremental approach might be more suitable. A practical compromise is to migrate feature by feature instead of layer by layer.
This first part outlines a structured methodology for gradually migrating a mobile application to Kotlin Multiplatform. The further your Android code is from Java and the Android ecosystem, and the closer it is to pure Kotlin, the easier the migration will be. Similarly, the more aligned the architecture and interface layers between Android and iOS, the greater the benefits of progressive migration per layer on iOS.
In the second part, we’ll share a real-world case study, covering challenges encountered and lessons learned.
📌 Stay tuned for the next part! 🚀