Kotlin 更新了一篇迁移指南:把一个 Jetpack Compose Android 应用迁到Kotlin Multiplatform。我们来看看是怎么回事。
先判断项目能不能迁
Android 项目不是只要用了 Kotlin 就能直接变成 KMP。
第一条判断:如果项目已经是 Kotlin + Jetpack Compose,迁移复杂度会明显降低。反过来,如果还有大量 Java、Android View、自定义 View 体系,迁移前就要先解决这些问题。
原因在 commonMain。
KMP 共享代码最后要放进 commonMain,这里不能放 Java 代码,也不能直接依赖 Android Framework。你可以在 Android 里继续调用 Java,但共享层不行。
一个典型问题是这类代码:
bash
// Android-only / JVM-only 思路
val key = Objects.hash(id, title, author)
val encoded = Uri.encode(url)
val now = java.time.Instant.now()
放在 Android 模块里没问题,搬到 commonMain 就会变成阻塞点。官方例子里,Jetcaster 就遇到了 Objects.hash()、Uri.encode() 和大量 java.time 使用。
迁移前要先把这类代码分成两种:
bash
能替换的:改成 Kotlin / Multiplatform 方案
不能替换的:隔离到 androidMain,再给 iOS / Desktop 写 actual 实现
否则,后面迁模块时会一直被编译错误打断。

依赖替换
Jetcaster 项目迁移里,大部分工作是识别 Android-only 依赖,并找到多平台替代品。
Jetcaster 里几个关键替换是:
bash
Dagger / Hilt -> Koin 4
Coil 2 -> Coil 3
ROME -> Multiplatform RSS Parser
JUnit -> kotlin-test
java.time -> kotlin.time + kotlinx-datetime
这个顺序比直接改 Gradle 插件更稳。
比如 DI,如果项目里到处都是 Hilt 注解,先把模块改成 KMP 并不能解决问题。commonMain 不能依赖 Android 注入入口,也不能依赖 Hilt 生成代码。官方例子选择先全局迁到 Koin 4,连 Android-only 的 mobile 入口模块也一起改。
迁 Hilt 时还有一个细节:清掉 /build 目录。原因是旧的 Hilt 生成代码可能还留在构建目录里,继续影响编译。
对 Android 项目来说,可以先做一张依赖表:
bash
库名 当前用途 是否 Android-only KMP 替代
Hilt DI 是 Koin / Metro
Coil 2 图片加载 Android 侧 Coil 3
Room 数据库 可迁 Room 2.7.0+
JUnit 单测 JVM 侧 kotlin-test
java.time 时间处理 JVM 侧 kotlinx-datetime
"先建 shared 模块"更重要。

业务模块从叶子节点开始
依赖处理完之后,才轮到模块适配。
官方示例里,Jetcaster 的简化模块关系大致是:
bash
:mobile
:core:data
:core:data-testing
:core:domain
:core:domain-testing
:core:designsystem
迁移顺序不是从 App 壳开始,而是从依赖关系里更底层、更少被其他模块反向牵扯的模块开始。
官方给的顺序是:
bash
:core:data
:core:data-testing
:core:domain
:core:domain-testing
:core:designsystem
这个顺序符合大多数 Android 多模块项目的真实情况。业务逻辑比 UI 更适合先共享,数据层和 domain 层比入口模块更容易切开。
一个 KMP 化后的模块通常会变成这种结构:
bash
core/data/src/
commonMain/
kotlin/
androidMain/
kotlin/
iosMain/
kotlin/
jvmMain/
kotlin/
commonMain 放真正可共享的 Repository、数据模型、接口和业务规则。平台差异放进 androidMain、iosMain、jvmMain。
Room 是一个很好的例子。
官方文档提到,Jetcaster 使用 Room 做数据库。Room 从 2.7.0 开始支持 Multiplatform,所以迁移不是把数据库全部重写,而是调整到 KMP 可用的写法,并用 expect/actual 处理平台相关部分。
可以用一个简化例子理解:
bash
// commonMain
expect class DatabaseFactory {
fun create(): AppDatabase
}
class PodcastRepository(
private val databaseFactory: DatabaseFactory
) {
private val database = databaseFactory.create()
}
bash
// androidMain
actual class DatabaseFactory(
private val context: Context
) {
actual fun create(): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"podcasts.db"
).build()
}
}
这段代码说明的是边界:Repository 可以共享,数据库创建细节不能假装没有平台差异。
官方例子里还加了一个 OnlineChecker 接口,用来包住"只在 Android 检查网络连接"的事实。在 iOS 入口真正接入前,它可以先是 stub。

实战
把官方迁移过程压缩成工程动作,大概是这样:
能先替换依赖,就不要先搬模块。能先迁业务层,就不要先碰复杂 UI。能按屏幕迁,就不要把所有页面锁在一次大重构里。
KMP 迁移最怕的是把"技术方向"变成"全项目重写"。官方这个 Jetcaster 示例给出的路径更像日常工程:每一步都尽量小,每一步都保持可运行。
最后
KMP 迁移对 Android 项目来说,不仅仅是把 android {} 改成 kotlin {}。
要做的是清理共享边界:依赖边界、平台边界、资源边界、UI 边界。其他的,Android、iOS、Desktop 入口只是最后接上去而已。