Kotlin 更新了一篇迁移指南:把一个 Jetpack Compose Android 应用迁到Kotlin Multiplatform。我们来看看是怎么回事。
先判断项目能不能迁
Android 项目不是只要用了 Kotlin 就能直接变成 KMP。
第一条判断:如果项目已经是 Kotlin + Jetpack Compose,迁移复杂度会明显降低。反过来,如果还有大量 Java、Android View、自定义 View 体系,迁移前就要先解决这些问题。
原因在commonMain。
KMP 共享代码最后要放进commonMain,这里不能放 Java 代码,也不能直接依赖 Android Framework。你可以在 Android 里继续调用 Java,但共享层不行。
一个典型问题是这类代码:
// 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使用。
迁移前要先把这类代码分成两种:
能替换的:改成 Kotlin / Multiplatform 方案 不能替换的:隔离到 androidMain,再给 iOS / Desktop 写 actual 实现否则,后面迁模块时会一直被编译错误打断。
依赖替换
Jetcaster 项目迁移里,大部分工作是识别 Android-only 依赖,并找到多平台替代品。
Jetcaster 里几个关键替换是:
Dagger / Hilt ->Koin4Coil2->Coil3ROME ->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 项目来说,可以先做一张依赖表:
库名 当前用途 是否 Android-only KMP 替代 Hilt DI 是 Koin / Metro Coil2图片加载 Android 侧 Coil3Room 数据库 可迁 Room2.7.0+ JUnit 单测 JVM 侧 kotlin-test java.time 时间处理 JVM 侧 kotlinx-datetime“先建 shared 模块”更重要。
业务模块从叶子节点开始
依赖处理完之后,才轮到模块适配。
官方示例里,Jetcaster 的简化模块关系大致是:
:mobile :core:data :core:data-testing :core:domain :core:domain-testing :core:designsystem迁移顺序不是从 App 壳开始,而是从依赖关系里更底层、更少被其他模块反向牵扯的模块开始。
官方给的顺序是:
:core:data :core:data-testing :core:domain :core:domain-testing :core:designsystem这个顺序符合大多数 Android 多模块项目的真实情况。业务逻辑比 UI 更适合先共享,数据层和 domain 层比入口模块更容易切开。
一个 KMP 化后的模块通常会变成这种结构:
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处理平台相关部分。
可以用一个简化例子理解:
// commonMainexpectclass DatabaseFactory{fun create(): AppDatabase}class PodcastRepository(private val databaseFactory: DatabaseFactory){private val database=databaseFactory.create()}// androidMain actual class DatabaseFactory(private val context: Context){actual fun create(): AppDatabase{returnRoom.databaseBuilder(context, AppDatabase::class.java,"podcasts.db").build()}}这段代码说明的是边界:Repository 可以共享,数据库创建细节不能假装没有平台差异。
官方例子里还加了一个OnlineChecker接口,用来包住“只在 Android 检查网络连接”的事实。在 iOS 入口真正接入前,它可以先是 stub。
实战
把官方迁移过程压缩成工程动作,大概是这样:
能先替换依赖,就不要先搬模块。能先迁业务层,就不要先碰复杂 UI。能按屏幕迁,就不要把所有页面锁在一次大重构里。
KMP 迁移最怕的是把“技术方向”变成“全项目重写”。官方这个 Jetcaster 示例给出的路径更像日常工程:每一步都尽量小,每一步都保持可运行。
最后
KMP 迁移对 Android 项目来说,不仅仅是把android {}改成kotlin {}。
要做的是清理共享边界:依赖边界、平台边界、资源边界、UI 边界。其他的,Android、iOS、Desktop 入口只是最后接上去而已。
[#Kotlin](javascript:😉 [#KMP](javascript:😉 [#ComposeMultiplatform](javascript:😉 [#Android开发](javascript:😉