| name | android-to-kmp |
| description | Incrementally extracting an existing Android (Kotlin + Jetpack) codebase into Kotlin Multiplatform `commonMain`, without forcing a big-bang rewrite. Use when introducing KMP into a shipping Android app. |
Migrating an Android App to KMP
Instructions
The goal is to share code without stopping feature delivery. Migrate bottom-up: pure logic first, frameworks last. UI usually stays Jetpack Compose on Android until commonMain is proven.
1. Add a :shared module to the existing project
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
}
kotlin {
androidTarget()
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
}
}
}
android { namespace = "com.example.shared"; compileSdk = 35; defaultConfig.minSdk = 24 }
Add :shared to settings.gradle.kts and have :app depend on it. Existing Android code keeps running; :shared is just an extra module.
2. Start with a pure-Kotlin layer
Pick code that has no Android imports today: domain models, validation, pricing rules, date math. Move the files from :app to :shared/src/commonMain/kotlin/... unchanged. Confirm ./gradlew :app:assembleDebug still builds.
data class Discount(val code: String, val percent: Int) {
fun apply(cents: Long): Long = cents - cents * percent / 100
}
3. Replace java.time / java.util.Date
commonMain cannot import java.*. Swap for kotlinx-datetime:
val now: LocalDateTime = LocalDateTime.now(ZoneId.of("UTC"))
val now: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.UTC)
For JSON, replace Moshi/Gson with kotlinx.serialization:
@Serializable data class UserDto(val id: Long, val name: String)
4. Replace frameworks incrementally
| Android library | KMP replacement |
|---|
Retrofit + OkHttp | Ktor client (OkHttp engine on Android) |
Room | SQLDelight |
WorkManager | Keep on Android; expose a shared interface |
SharedPreferences | Multiplatform-Settings or a KeyValueStore IF |
Moshi / Gson | kotlinx.serialization |
Coil | Coil 3 (KMP) |
Where no KMP equivalent exists, define an interface in commonMain and keep the Android implementation in androidMain.
5. Migrate a repository
interface ArticleRepository {
suspend fun latest(): List<Article>
fun observeLatest(): Flow<List<Article>>
}
class ArticleRepositoryImpl(
private val api: ArticleApi,
private val dao: ArticleDao,
) : ArticleRepository { }
Replace the Retrofit interface + Room DAO usage inside the impl step by step. Keep the ViewModel on Android pointing at ArticleRepository the whole time.
6. Move ViewModels (optional, later)
Once androidx.lifecycle:lifecycle-viewmodel is on KMP (2.8+), ViewModels can live in commonMain:
class FeedViewModel(private val repo: ArticleRepository) : ViewModel() {
val state = repo.observeLatest()
.map { FeedState(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), FeedState())
}
On Android, keep using by viewModels(). Don't introduce iOS until you've validated the shared ViewModel on Android.
7. Adding iOS
Only when :shared builds cleanly for androidTarget() and all dependencies have iOS counterparts, add:
listOf(iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework { baseName = "Shared"; isStatic = true }
}
sourceSets.iosMain.dependencies { implementation(libs.ktor.client.darwin) }
Generate the XCFramework, consume from a new Xcode project, and build the iOS UI afresh.
8. Anti-patterns
- Don't move Compose screens to
commonMain before the domain/data layers are stable there.
- Don't ship a partial migration where the same logic exists in both
:app and :shared.
- Don't enable iOS targets just to "see if it works" — every unnecessary target multiplies CI time.
Checklist