In 2026, many businesses still run mature Android applications that were originally built with Java and native Android APIs. When the demand for iOS and Web users grows, converting that codebase to Kotlin Multiplatform Mobile (KMM) becomes a strategic imperative. This guide offers a practical, step‑by‑step blueprint for moving a legacy Android app to KMM while ensuring continuous integration, robust testing, and smooth handling of platform quirks.
1. Assessing the Legacy Codebase
Before you touch a single line of code, you need a clear picture of the current state of the Android project. Start by:
- Documenting the architecture: MVVM, MVP, or custom patterns.
- Cataloguing third‑party libraries, especially those that are platform‑specific (e.g., AndroidX, Google Play Services).
- Evaluating the test coverage and identifying gaps.
- Mapping out data persistence layers, networking stacks, and any embedded native components.
With this inventory, you can pinpoint which modules are safe to share, which need refactoring, and which may require replacements in the KMM ecosystem.
2. Setting Up the KMM Project Skeleton
Once the assessment is complete, create a fresh KMM project that follows the recommended folder layout:
project-root/ ├─ androidApp/ ├─ iosApp/ ├─ shared/ │ ├─ src/commonMain/ │ ├─ src/commonTest/ │ ├─ src/androidMain/ │ ├─ src/iosMain/ │ └─ src/jvmTest/
The shared module houses all platform‑agnostic code, while androidMain and iosMain keep platform‑specific implementations. Using Gradle’s kotlinMultiplatform plugin, you’ll define targets for Android, iOS, and any additional platforms you plan to support.
3. Gradle Configuration & Dependency Management
Gradle is the heart of a KMM project. To streamline migration:
- Upgrade the Gradle wrapper to the latest stable version that supports Kotlin 2.0 (or the current release).
- Configure the
kotlinblock with the desired JVM target (e.g.,jvmTarget = "17") and the iOS ABI. - Replace AndroidX libraries with their multiplatform equivalents, such as
kotlinx-coroutines-coreorktor-client-corefor networking. - Use
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")to replace Java’sjava.timewhere cross‑platform usage is required. - Keep the dependency version catalog (e.g.,
libs.versions.toml) to avoid version drift.
Example build.gradle.kts snippet for the shared module:
kotlin {
android()
ios()
jvm("desktop") // optional for desktop testing
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.coroutines.core)
implementation(libs.ktor.client.core)
}
}
val androidMain by getting {
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.viewmodel)
}
}
val iosMain by getting {
dependencies {
implementation(libs.ktor.client.ios)
implementation(libs.compose.runtime)
}
}
}
}
4. Migrating Core Modules to Shared Code
Begin by moving non‑UI, business‑logic modules into the commonMain source set. Follow these practices:
- Extract use‑cases and repositories that interact with data sources.
- Replace platform‑specific services (e.g.,
SharedPreferences) with interfaces defined incommonMainand provide concrete implementations inandroidMainandiosMain. - Use Kotlin’s
expect/actualmechanism for functions that need platform nuance, such as file I/O or notification handling. - Leverage
expect/actualfor date/time utilities to usekotlinx-datetimeon Android andDateComponentson iOS. - Apply Dependency Injection via Koin or Dagger (with multiplatform support) to keep the shared module testable.
Keep the migration incremental: move one feature at a time, run unit tests, and validate on both Android and iOS.
5. Handling Platform‑Specific Code and Quirks
Even with a shared core, certain areas will remain platform‑specific:
- UI layers continue to use Jetpack Compose for Android and SwiftUI for iOS, but you can share composables with
expect/actualwrappers. - Permission management varies; encapsulate permission logic in a
PermissionManagerinterface. - File system paths differ; provide separate implementations in
androidMainandiosMainfor reading/writing files. - Hardware sensors may require native APIs; use
expect/actualor platform-specific extensions.
Document each platform quirk so future developers can quickly understand why a particular implementation exists.
6. Continuous Integration for Kotlin Multiplatform
Automating builds and tests across all targets is essential. Set up a CI pipeline (GitHub Actions, GitLab CI, or Bitbucket Pipelines) with these steps:
- Checkout the repository.
- Set up the JDK (preferably 17) and Android SDK.
- Run
./gradlew buildto compile shared, Android, and iOS modules. - Execute
./gradlew testfor JVM tests, and./gradlew iosTestfor XCTest integration. - Generate code coverage reports with
jacocofor JVM andkoverfor multiplatform. - Archive build artifacts: APK, AAB, and iOS IPA for further QA steps.
Use matrix jobs to run tests on multiple Android API levels and iOS simulators. This guarantees that platform‑specific regressions are caught early.
7. Testing Strategy Across Platforms
Testing is a cornerstone of successful migration. Adopt the following approach:
- Unit tests in
commonTestcover pure business logic, usingkotlin.testorJUnit5. - Use
mockkfor mocking dependencies within shared code. - For Android, add instrumented tests in
androidTestto validate UI flows and dependency injection. - For iOS, write XCTest cases that invoke the Swift entry points for shared code. Use
kotlinx-coroutines-testto manage coroutine dispatchers. - Implement snapshot tests for UI components, ensuring visual consistency across platforms.
- Integrate static analysis tools: Detekt for Kotlin, SwiftLint for Swift, and SonarCloud for code quality.
Maintain a target coverage of at least 80% in shared modules, and 70% in platform modules.
8. Release Pipeline and Deployment
With CI generating build artifacts, the next step is deployment:
- Use Fastlane for Android (Gradle tasks) and iOS (Xcodebuild, TestFlight).
- Configure Gradle to produce signed APKs and AABs automatically using
signingConfigs. - Set up a
releasebranch that triggers a full CI run, publishes to internal test tracks, and optionally to the Play Store and App Store. - Track versioning with Semantic Versioning, where the patch number increments on each successful CI run.
- Automate changelog generation by parsing commit messages with Conventional Commits.
9. Post‑Migration Checklist
After the codebase is migrated and stable:
- Remove unused legacy modules and Gradle plugins.
- Update documentation to reflect the new KMM structure.
- Archive legacy code in a separate Git tag for future reference.
- Schedule a training session for the team on multiplatform best practices.
- Plan incremental feature rollouts to the iOS side to validate the user experience.
10. Conclusion
Migrating a legacy Android app to Kotlin Multiplatform Mobile in 2026 is a structured yet flexible process. By assessing the current codebase, establishing a clean KMM skeleton, managing dependencies, migrating core logic, handling platform quirks, and integrating robust CI, testing, and deployment pipelines, teams can achieve a shared codebase that serves both Android and iOS users efficiently. The blueprint outlined here provides a practical framework to navigate the complexities of migration while ensuring quality, maintainability, and future scalability.
