Theodo apps

Migration of an iOS/Android Application to Kotlin Multiplatform: Methodology and Key Steps

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.

🔍 0. Identifying Differences Between Platforms

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.

  1. List all functional differences between the two applications.
  2. Determine if they are intentional or not.
  3. For each unintended difference, define a unified version to adopt in the KMP architecture.

➡️ 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.

🛠 1. Preparing the Android Project for Migration

Before starting, some updates are necessary:

  • Ensure you have recent versions of Kotlin, KSP, and Gradle to leverage the latest optimizations and avoid compatibility issues.
  • Some third-party libraries used on Android (Koin, Room, Coil, etc.) are KMP-compatible. Update them before starting.
  • Jetpack Compose evolves quickly; updating it allows you to leverage the latest improvements (if migrating the UI).

➡️ The goal is to ensure a smooth transition without unexpected technical roadblocks.

🔄 2. Creating the Multiplatform Project

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:

  • Convert one of the Git repositories (Android/iOS) into a shared repository containing both the Android and iOS applications with the future shared code. This solution requires some handling to preserve the Git history of both projects (e.g., using Git submodules).

This solution has its limitations. To go further, check out this article from TouchLab (a leader in KMP) discussing their tool Gitportal.

👁️ 3. Identifying the Easiest Feature to Migrate

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?

  • If your application has flows, look at the end of flows or secondary flows.
  • If your app has a bottom navigation bar, choose the tab least connected to core functionalities.

📦 4. Migrating the Model Layer of a 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?

  1. Convert as much code as possible using pure Kotlin APIs instead of Java or Android-specific APIs (e.g., replace java.util.UUID with kotlin.uuid.Uuid).
    • This applies to all module migrations.
    • This is the core of Android → KMP migration: replacing JVM-specific and Android SDK-specific code with KMP-compatible code that compiles across all platforms.
  2. In the shared multiplatform module, create a 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.
    • Use your multiplatform module to gradually move subcomponents, making commits of appropriate size.
  3. In your Android and iOS code, replace imports of the old model classes with imports from the shared module.
    • Using the same package in the KMP code as in Android helps avoid this step and minimizes changes.

🔄 5. Migrating Cross-Cutting Modules

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.

🔗 6. Migrating the Data Layer of the Same Feature

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.

  1. Choose a KMP library for network calls and the database.
  2. Migrate your Data layer classes using KMP libraries.
  3. Create adapters to convert common reactive data into platform-specific reactive data for Android or iOS. Your business logic layer will request reactive data from your Data layer, so this layer will need these adapters.

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:

  • Create specific components to manage interactions between feature data.
  • Reassess which feature should be migrated first and choose one with fewer dependencies.
  • Migrate data from other features at the same time if feasible.

🏗 7. Migrating the Business Logic Layer of the Same Feature

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).

🖼 8. Migrating the Presentation Layer of the Same Feature

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

  • Identify parts that are too tightly coupled to frameworks and adapt them.
  • Validate every modification to prevent regressions.
  • Remove the adapters from step 5 if they are no longer needed.

🎨 9. Migrating the View Layer of the Same Feature

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.

✅ 10. Finalizing the Migration

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.

🤔 Should iOS Be Migrated Gradually?

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.

Conclusion

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! 🚀

Développeur mobile ?

Rejoins nos équipes