API 30 老项目从纯 Java 切纯 Kotlin:用 AI 辅助跨语言迁移的实操指南

4 阅读8分钟

上一篇聊了 MVP 到 MVI 的架构改造,这次把镜头拉近,只聊一件事:编译环境停在 API 30、代码全是 Java 的老项目,怎么切到纯 Kotlin,并且让功能在最短时间内稳住。

背景很具体:compileSdk 30,没有用 Compose,没有协程,大概率还在用 AsyncTask 或 Handler 做异步。这种项目切 Kotlin,最大的风险不是语法不会写,而是混编边界上的语义差异——Java 和 Kotlin 看起来能互相调用,但空安全、序列化、注解处理这三块,只要有一个没对齐,运行时崩溃就来了。

AI 在这里能帮大忙,但用法要对。


一、先锁版本:API 30 决定了你的 Kotlin 上限

不要拿到项目就改 compileSdk。API 30 环境下,AGP 版本通常偏低,对应的 Kotlin 版本也有限制。强行上最新 Kotlin 编译器,Gradle 插件可能直接不兼容。

安全区间:AGP 7.x + Kotlin 1.6.x 到 1.8.x 的组合在 compileSdk 30 上最稳。Kotlin 官方声明兼容 JDK 6 以上,Android 侧 API 30 完全没问题。但高版本 Kotlin 编译出的字节码可能要求更高版本的 AGP 或 Gradle,所以先把 Gradle 和 AGP 的版本对应关系查清楚,再决定 Kotlin 版本。

这一步可以交给 AI:把当前 build.gradle 里的 AGP 版本、Gradle wrapper 版本贴给模型,问“这个组合最高能用到哪个 Kotlin 版本且 compileSdk 30 不报错”。比手动翻兼容性表格快得多。

不要同时升 compileSdk 和切 Kotlin。两个变量一起动,出了问题根本分不清是谁的锅。先保持 API 30 不动,把 Kotlin 跑通,再考虑要不要升 targetSdk。


二、空安全:AI 最容易“帮倒忙”的地方

Kotlin 的空安全和 Java 的平台类型是混编最大的坑。Android 官方文档写得很清楚:如果 Java 侧没有 @Nullable / @NonNull 注解,Kotlin 会把对应值推断为平台类型,编译器不强制判空,运行时该 NPE 还是 NPE。

问题在于,你用 IDE 的 Convert Java File to Kotlin File 或让 AI 帮你转文件时,生成的 Kotlin 代码往往保留了平台类型的“裸奔”状态。AI 倾向于把 Java 的 String 直接写成 String,而不是 String?,因为它“看起来”像是非空的。

实操策略:

  1. 先补注解,再转文件。在 Java 侧给所有公开方法的参数和返回值加上 @Nullable / @NonNull(可以用 AI 批量分析哪些方法可能返回 null)。然后再执行转换,Kotlin 编译器才能生成正确的可空类型。
  2. AI 转完的文件,人工过一遍每个来自 Java 的返回值。问自己:这个值在旧 Java 代码里有没有判空?如果有,Kotlin 里就应该是 ?。
  3. 不要信任 AI 生成的 !! 。如果模型在转换时加了 !!,大概率是在掩盖它不确定的可空性。把它改成 ?. 配合合理的默认值,或者补上 requireNotNull 并给出有意义的错误信息。

Kotlin 官方文档也提醒了,过度使用 ?.let 会让代码可读性下降,不如直接写 if (x != null) 然后利用智能转换。迁移初期,选择更直白的写法,别为了“Kotlin 味”牺牲可排查性。


三、Gson + data class:最隐蔽的运行时杀手

老 Java 项目大概率用 Gson 做 JSON 解析。Java 的模型类有默认构造器和 setter,Gson 用反射填字段,一直没出过问题。但你把它转成 Kotlin 的 data class 之后,字段变成 private final,Gson 的反射机制可能静默失败——不崩溃,但所有字段都是默认值或 null。

这是混编迁移中最典型的坑之一,而且 AI 转换时完全不会提醒你,因为它只关心语法正确。

解法:给 data class 里每个参与 JSON 映射的字段加 @JvmField 和 @SerializedName。

kotlin

data class UserProfile(
    @JvmField @SerializedName("nick_name") val nickName: String = "",
    @JvmField @SerializedName("avatar_url") val avatarUrl: String = ""
)

@JvmField 让字段以 public 形式暴露给 Gson 的反射,@SerializedName 确保字段名映射正确。如果你在用 Moshi 或 Kotlinx Serialization,机制不同,需要单独确认。

AI 辅助检查:把模型类的 Kotlin 代码贴给模型,问“这个 data class 用 Gson 反序列化有没有风险”。好的模型会指出 @JvmField 缺失的问题。但不要只依赖一次问答,写一个简单的单元测试:用真实的 JSON 字符串反序列化,断言关键字段不为默认值。这个测试跑通,比任何 AI 分析都可靠。


四、注解处理器:kapt 是过渡,KSP 是方向

如果项目用了 Room、Dagger、Glide 注解处理器,切 Kotlin 后第一件事是把 annotationProcessor 改成 kapt。这是让 Java 注解处理器在 Kotlin 代码上工作的标准做法。

但 kapt 的问题很明显:它通过生成 Java 桩代码来运行处理器,构建速度显著变慢,而且不理解 Kotlin 特有的语言结构。

更优解是 KSP。KSP 直接分析 Kotlin 源码,速度比 kapt 快最多 2 倍,且对 Kotlin 类型(包括可空性)的理解更准确。主流库基本都已支持:Room、Dagger/Hilt、Moshi、Glide 都有 KSP 版本。

迁移节奏:

  • 第一阶段:用 kapt 让项目先跑起来,不追求构建速度。
  • 第二阶段:查你用的每个注解处理库是否支持 KSP。Android 官方文档有支持列表。支持的逐个把 kapt(...) 替换为 ksp(...),不支持的就留在 kapt。
  • kapt 和 KSP 可以在同一个模块里共存,逐库迁移,不需要一刀切。

AI 在这里的用途:把 dependencies 块贴给模型,让它标出哪些依赖可以用 KSP 替换、对应的 artifact 名称是什么、版本号怎么对齐。比手动翻每个库的文档快很多。但版本号一定要自己验证,AI 给的版本可能已经过时或不存在。


五、用 AI 加速迁移的正确姿势

前面说的都是“坑”,现在说怎么让 AI 真正提速。

不要做的事:把整个 Java 文件丢给 AI 说“帮我转成 Kotlin”。得到的代码能编译,但平台类型裸奔、Gson 字段没注解、@JvmOverloads 缺失,后续排查成本远超省下的时间。

应该做的事:

第一步,让 AI 做依赖分析。 把项目的模块结构和关键类的 import 列表给模型,问“如果我按 A → B → C 的顺序转 Kotlin,中间哪些文件会编译失败”。这比你自己画依赖图快得多。学术研究也证实了,文件级别的独立翻译在大型仓库中成功率很低,因为跨文件依赖不一致是主要失败原因。虽然我们不是做自动化仓库迁移,但这个洞察对人工分批有指导意义。

第二步,按“无依赖 → 低依赖 → 高依赖”的顺序分批转。 先转纯工具类、扩展函数、常量类。再转 Repository 和数据模型。最后动 Activity/Fragment 和它们直接依赖的类。每转完一批,编译通过 + 冒烟测试通过再进下一批。

第三步,用 AI 做转换后的“二次审查”。 语法转换让 IDE 做,AI 用来回答具体问题:

  • “这个 Kotlin 类会被 Java 代码调用,有没有缺失的 @JvmStatic 或 @JvmOverloads?”
  • “这个 data class 用 Gson 反序列化安全吗?”
  • “这个方法的参数在旧 Java 代码里有没有可能传 null?Kotlin 签名应该怎么改?”

第四步,AI 辅助写回归测试。 迁移前,让 AI 根据旧 Java 代码的关键路径生成一批单元测试(针对 Repository 和工具类)。迁移后跑这些测试,通过即证明行为一致。这比人工对比代码可靠得多。


六、功能稳定的最低验收标准

“最快做到功能稳定”不等于“跑通就行”。切 Kotlin 后,至少验证这三层:

编译层:整个项目 assembleDebug 成功,没有 warning 级别的平台类型提示被忽略。

单元测试层:旧 Java 测试如果还在,确保它们能跑。新写的 Kotlin 测试重点覆盖:数据模型的序列化/反序列化、Repository 的返回值边界(null、空列表、异常)、工具类的输入输出。

运行时层:核心流程手动走一遍。特别注意所有涉及 JSON 解析的页面(登录、列表、详情),因为 Gson + data class 的问题只有运行时才暴露。另外检查所有从 Java 调 Kotlin 的入口,确认 @JvmStatic / @JvmOverloads 没有遗漏导致编译期没报但运行时行为异常。


最后一句

API 30 的纯 Java 项目切 Kotlin,技术难度其实不高,高的是对混编边界语义差异的敬畏。AI 能帮你写代码、查兼容性、生成测试,但它不会替你承担“这个 !! 会不会在线上崩”的责任。

分批转、每批验证、优先处理序列化和注解处理这两个运行时风险点,是让项目在最短时间内回到稳定状态的最短路径。别追求“一次性全转完”,那往往是返工的开始。