
Kotlin 2.4.0 发布了,更新挺多的。语言、标准库、JVM、Gradle、Compose 编译器,还有 Native、Wasm、JS 这些平台都有变动。
对 Android 开发者来说,不是所有改动都要看。
先关注这几块:构建版本要求、Compose 编译器的变化、Kotlin/JVM 的行为调整、标准库的改进,还有少数会影响库作者和大型项目的语言特性。
这篇文章就按这个优先级来讲。最后会提一下那些和日常 Android 开发关系不大的更新,可以快速略过。
Android 开发者应该先看的部分
1. 版本和构建要求
升级之前先确认几件事:
- IDE 要先更新。最新版的 IntelliJ IDEA 和 Android Studio 都已经内置了 Kotlin 2.4.0。
- AGP 最低要求 8.5.2。这个比较关键,低于这个版本的项目要先升 AGP。
- Gradle 兼容范围是 7.6.3 到 9.5.0。更高的版本可能也能用,但可能会有弃用警告。
另外,多模块项目要注意一个变化:Kotlin 2.4.0 把不同平台的默认 module name 统一成了 {group}:{project_name}。如果 JVM 项目想保持原来的命名,可以这样配置:
kotlin
kotlin {
compilerOptions.moduleName(project.name)
}
跨平台项目里的 JVM target 可以这样:
kotlin
kotlin {
jvm {
compilerOptions.moduleName(project.name)
}
}
2. Compose 编译器的增量编译
Compose 编译器对 internal 声明的处理变了一点。
之前,如果 @Composable 函数用了另一个文件里的 internal 类型作为参数,稳定性推断有时不够准确。现在改成在运行时推断,这样即使那个类没被重新编译,Compose 也能拿到最新的稳定性信息。
最终的稳定性结果不会变,@Composable 函数的行为也不会变。
不过有个副作用:如果参数里用了跨文件的 internal 类,产物体积可能会变大。因为编译器要同时编码稳定和不稳定两条路径。好在 R8 这类全应用优化工具能删掉不需要的路径。
还有几个 Compose feature flag 要注意:
StrongSkipping、IntrinsicRemember和相关的 DSL 属性已经升级到DeprecationLevel.ERROR,Kotlin 2.5.0 会删掉。OptimizeNonSkippingGroups和PausableComposition现在已经弃用,计划 Kotlin 2.6.0 删除。
如果项目构建脚本里手动配过这些 flag,升级前记得清理。
3. 语言特性
Kotlin 2.2.0 和 2.3.0 引入的一些特性,在 2.4.0 里变成了 Stable:
- 上下文参数(context parameters)
- 属性上的
@all元目标 - 注解使用目标的新默认规则
- 显式 backing fields
对日常 Android 开发来说,这几项的影响主要是:写 DSL、组件 API、注解密集的代码时,可用的方式更多了,而且不用再加 @OptIn。
顺便提一句:上下文参数的显式参数和 callable references 还是实验性的。
上下文参数的显式参数
Kotlin 2.3.20 调整了 context parameters 的重载解析规则,导致如果两个重载只靠 context parameter 区分,调用时可能会有歧义。
Kotlin 2.4.0 引入了显式上下文参数,可以在调用时指定到底用哪个:
kotlin
class EmailSender
class SmsSender
context(emailSender: EmailSender)
fun sendNotification() {
println("Sent email notification")
}
context(smsSender: SmsSender)
fun sendNotification() {
println("Sent SMS notification")
}
context(defaultEmailSender: EmailSender, defaultSmsSender: SmsSender)
fun notifyUser() {
// 选择带 EmailSender 上下文参数的重载
sendNotification(emailSender = defaultEmailSender)
// 选择带 SmsSender 上下文参数的重载
sendNotification(smsSender = defaultSmsSender)
}
这个特性还可以减少 context() 带来的嵌套。如果同一组上下文要在多个调用里复用,继续用 context() 会更合适。
目前还是实验性的,需要加编译器参数:
kotlin
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexplicit-context-arguments")
}
}
集合字面量
Kotlin 2.4.0 引入了实验性的集合字面量。简单说,就是可以用方括号 [] 创建集合:
kotlin
fun main() {
// 带显式类型声明的可变列表
val shapes: MutableList<String> = ["triangle", "square", "circle"]
println(shapes)
// [triangle, square, circle]
}
如果没有明确类型,编译器会默认创建 List:
kotlin
fun main() {
val fruit = ["apple", "banana", "cherry"]
println(fruit)
// [apple, banana, cherry]
}
还支持自定义类型,只要提供 operator fun of:
kotlin
class DoubleMatrix(vararg val rows: Row) {
companion object {
operator fun of(vararg rows: Row) = DoubleMatrix(*rows)
}
class Row(vararg val elements: Double) {
companion object {
operator fun of(vararg elements: Double) = Row(*elements)
}
}
}
这样就能用嵌套方括号:
kotlin
fun main() {
val identityMatrix: DoubleMatrix = [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
]
}
启用方式:
kotlin
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcollection-literals")
}
}
语法挺顺手,但现在还是实验性的。普通 Android 业务项目不建议为了它专门开编译器参数;库、DSL 或者实验项目可以先试试。
如果想深入了解,我前几天刚写了一篇文章,任君品阅。
编译期常量的改进
Kotlin 2.4.0 扩展了编译期常量的支持范围,包括无符号类型运算、标准库里的字符串函数(像 .lowercase()、.uppercase()、.trim()),还有枚举常量的 .name 属性等。
kotlin
kotlin {
compilerOptions {
freeCompilerArgs.add("-XIntrinsic-const-evaluation")
}
}
这个能力目前也是实验性的。对普通 Android 项目的收益不算大,但如果你维护注解、代码生成、常量密集的基础库,留意一下即可。
高阶函数未使用结果检查
Kotlin 2.4.0 新增了实验性的 returnsResultOf() 契约,用来改进"返回值没被使用"的检查。
它能帮检查器区分两类情况:一种是返回值本来就可以忽略,另一种是返回值有意义但开发者忘了用。
kotlin
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
inline fun <T, R> T.customLet(block: (T) -> R): R {
contract {
returnsResultOf(block)
}
return block(this)
}
示例:
kotlin
fun handleNullablePackageName(packageName: String?, builder: StringBuilder) {
// 不会触发警告:append() 的返回值可以忽略
packageName?.customLet { builder.append(it) }
// 会触发警告:返回的字符串没被使用
packageName?.customLet { "kotlin.$it" }
}
启用方式:
kotlin
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xallow-returns-result-of")
}
}
Android 项目里 let、run、also 这类高阶函数用得很多,这个方向有价值。
4. 二进制兼容
如果只写 App 业务代码,@IntroducedAt 可能用不上。但如果你维护 SDK、组件库、设计系统、Compose 组件库,这个注解很重要。
问题是这样的:一个已经发布的 Kotlin API,如果后来给函数加了新的可选参数,源码调用看着没问题,但旧版本编译出来的二进制代码可能找不到旧签名。
以前常见做法是手写旧重载,或者用 @JvmOverloads,但维护成本都不低。
@IntroducedAt 可以给新增参数标版本号,编译器会据此生成版本相关的隐藏重载。
kotlin
@OptIn(ExperimentalVersionOverloading::class)
fun Button(
label: String = "",
color: Color = DefaultColor,
@IntroducedAt("1.1") borderColor: Color = DefaultBorderColor,
@IntroducedAt("1.2") borderStyle: Style = DefaultBorderStyle,
@IntroducedAt("1.2") borderWidth: Int = 1,
onClick: () -> Unit
) {
// 函数体
}
注意:@IntroducedAt 和 @JvmOverloads 都会生成重载,同时使用可能冲突。编译器会给出警告;如果强行 suppress,编译器会优先用 @IntroducedAt 生成的重载。
我没忍住吐槽一下:这个方案一点都不优雅,这写出来的函数,感觉后续又臭又长,还不如在注解中进行警告提示。
5. 标准库
UUID API 稳定
kotlin.uuid.Uuid 终于进入稳定状态了。以前 Android 项目里直接用 java.util.UUID,现在如果写的是 common 代码,或者计划迁移到 Kotlin Multiplatform,推荐使用 kotlin.uuid.Uuid。
不过 V4 和 V7 UUID 的生成函数还是实验性的。
检查集合是否有序
标准库新增了一组判断集合是否有序的函数:
isSorted()isSortedDescending()isSortedWith(comparator)isSortedBy(selector)isSortedByDescending(selector)
这些函数遇到第一组乱序元素就返回,不会无意义地遍历完整集合。
kotlin
data class User(val name: String, val age: Int)
fun main() {
val numbers = listOf(1, 2, 3, 4)
println(numbers.isSorted())
// true
val users = listOf(
User("Alice", 24),
User("Bob", 31),
User("Charlie", 29),
)
println(users.isSortedBy(User::age))
// false
}
这类 API 适合用在列表校验、分页数据检查、测试断言、缓存顺序校验这些场景。
无符号整数转 BigInteger
JVM 上新增了 UInt.toBigInteger() 和 ULong.toBigInteger()。以前要通过字符串绕一圈,现在可以直接转:
kotlin
fun main() {
val unsignedLong = Long.MAX_VALUE.toULong() + 1uL
val unsignedInt = UInt.MAX_VALUE
println(unsignedLong.toBigInteger())
// 9223372036854775808
println(unsignedInt.toBigInteger())
// 4294967295
}
如果涉及协议、加密、二进制解析、超大数字处理,现在更方便了。
map fallback
这组 API 对 Android 开发比较实用,尤其是缓存和数据层。
以前 Map.get(key) 返回 null 时,很难直接知道是"key 不存在",还是"key 存在但 value 就是 null"。Kotlin 2.4.0 新增了两组函数来区分:
getOrElseIfNull()/getOrPutIfNull():key 不存在或 value 为null时使用默认值getOrElseIfMissing()/getOrPutIfMissing():只有 key 不存在时才使用默认值
kotlin
@OptIn(ExperimentalStdlibApi::class)
fun main() {
val mapForNull = mutableMapOf<String, String?>("key" to null)
val mapForMissing = mutableMapOf<String, String?>("key" to null)
// 替换 null 值
println(mapForNull.getOrPutIfNull("key") { "default" })
// default
println(mapForNull)
// {key=default}
// 因为 key 存在,保留 null 值
println(mapForMissing.getOrPutIfMissing("key") { "default" })
// null
println(mapForMissing)
// {key=null}
}
缓存场景特别直观。比如接口查询结果允许是 null,你仍然想把这个 null 缓存下来,避免重复请求:
kotlin
data class Response(val body: String)
class Service {
var queryCount = 0
fun query(key: String): Response? {
queryCount += 1
return null
}
}
@OptIn(ExperimentalStdlibApi::class)
fun main() {
val service = Service()
val cache = mutableMapOf<String, Response?>()
fun getCachedResponseOrQuery(key: String): Response? =
cache.getOrPutIfMissing(key) { service.query(key) }
// 缓存里没有,调用 service
getCachedResponseOrQuery("key")
println(cache)
// {key=null}
// 缓存里有 null,不再调用 service
getCachedResponseOrQuery("key")
println(service.queryCount)
// 1
}
比自己写 containsKey() 加 get() 的组合清楚多了,也不容易写错,少了个判断,能省不少事儿!
6. Kotlin/JVM
Kotlin 2.4.0 支持 Java 26,可以生成 Java 26 字节码。普通 Android 项目短期内不太会直接用 Java 26 target,但这说明 Kotlin/JVM 跟上了新的 Java 版本。
有个变化是:注解会默认写入 Kotlin metadata。
Kotlin 2.2.0 已经让 Kotlin Metadata JVM library 可以读取 metadata 里的注解。
Kotlin 2.4.0 开始,编译器默认把注解写进 metadata,同时也写进 JVM bytecode。这样注解处理器和工具可以在 metadata 层面理解这些注解,不用依赖反射或修改源码。
对 Android 开发来说,这对 KSP、kapt、代码生成、序列化、依赖注入、路由、数据库 ORM 这些工具链都可能有长远影响。业务代码不一定要改,但如果维护编译期工具,在 2.4.0 及以上的版本,不要忘了这个默认行为。
7. kapt:可以排除不必要的注解处理器
Kotlin 2.4.0 给 kapt 加了 includeCompileClasspath 配置项。设为 false 后,没有显式写在 annotation processor 配置里的处理器,会从 kapt 处理里排除。
例如,Maven 配置示例:
xml
<execution>
<id>kapt</id>
<goals><goal>kapt</goal></goals>
<configuration>
<includeCompileClasspath>false</includeCompileClasspath>
<sourceDirs>...</sourceDirs>
<annotationProcessorPaths>...</annotationProcessorPaths>
</configuration>
</execution>
也可以通过 properties 配置:
xml
<properties>
<kapt.include.compile.classpath>false</kapt.include.compile.classpath>
</properties>
如果没有设置这个选项,而 kapt 检测到 compile classpath 上有未显式声明的 annotation processor,会出弃用警告。
Android Gradle 项目里,长期方向还是能迁 KSP 就迁 KSP。但只要还在用 kapt,这类 classpath 收敛就值得关注,尤其是大型项目和构建性能敏感项目。
8. 破坏性变更:K1 和 -language-version=1.9 不再支持
Kotlin 2.4.0 不再支持 -language-version=1.9,也就是 K1 编译器已经不支持了。
如果你的 Android 项目还依赖旧编译器行为、旧插件,或者某些 kapt/KSP 插件没跟上 K2,需要在升级前做一次完整构建验证。
另外,Kotlin Gradle Plugin 里的 binary compatibility validation DSL 做了简化,部分旧 DSL 被弃用。如果项目里启用了二进制兼容性检查,也要顺手检查一下配置。
华丽的分割线
和普通 Android 开发关系不大的部分
下面这些更新和"日常 Android App 开发"关系不大。除非项目刚好涉及对应平台或工具链,了解一下即可。
Kotlin/Native
Kotlin/Native 默认启用了 CMS GC,减少 GC 暂停,对 Compose Multiplatform 的 iOS UI 应用更有价值。还降低了去虚拟化分析阶段的内存消耗,支持 Xcode 26.4,LLVM 更新到了 21。
Apple target 的最低版本也提高了:iOS 和 tvOS 提到 15.0,macOS 提到 12.0,watchOS 提到 8.0。做 KMP iOS 端的团队要认真看;只做 Android App 的可以略过。
Swift Export 进入 Alpha,改进了 structured concurrency 支持,kotlinx.coroutines 的 Flow 可以导出成 Swift 的 AsyncSequence。这对 KMP 共享业务逻辑很重要,但对纯 Android 项目没有直接影响。
Swift package import 也属于 KMP iOS 侧能力,普通 Android 项目不用关注。
Kotlin/Wasm
Kotlin/Wasm 的增量编译现在默认启用,Chrome DevTools 里的内部变量展示也更清晰,还加了 WebAssembly Component Model 的实验性支持。
这些变化主要服务浏览器、serverless 或 Wasm 组件化场景。Android 开发者只有在做 Kotlin/Wasm 或 Compose Multiplatform Web 时才需要深入看。
Kotlin/JS
Kotlin/JS 支持把 value class 导出到 JavaScript/TypeScript,内联 JS 代码时支持 ES2015 语法,TypeScript 导出的型变和接口导出也有改进。
这些更新对 Web 前端和 JS 互操作项目有用,对 Android App 本身影响很小。
Maven
Kotlin Maven 插件支持 Java 与 JVM target 自动对齐,也支持 Maven Toolchains。
Android 项目基本都用 Gradle,所以这部分一般不用细看。除非你维护的是服务端 Kotlin、命令行工具,或者某些共享库还在用 Maven 构建。
Build tools API
Build tools API 加了类型安全的编译器选项抽象、非源码输入的增量编译跟踪、ABI validation 支持、编译器消息渲染定制,还有 Kotlin daemon 日志配置。
这些内容主要给构建系统、IDE、插件作者看。普通 Android 业务开发者知道 Kotlin 构建工具在继续完善就行。
klib 编译和部分库链接
Kotlin 2.4.0 让 Native、JS、Wasm 的 .klib 编译阶段内联行为更接近 JVM,并且让 partial library linkage 始终启用。
这对 Kotlin Multiplatform 库作者重要,对纯 Android JVM target 项目影响不大。
Power-assert
Power-assert 加了新的运行时库和 @PowerAssert 注解,让断言函数更容易被编译器插件发现。
如果测试里用 Power-assert,可以了解一下。大多数 Android 项目如果没有引入这个插件,可以先不管。
我要不要升级
我这个开发比较激进:升,为什么不升?现在不升将来也要升,不要考虑那么多,梭哈一把,升!
开个玩笑。
如果你的 Android 项目已经在 AGP 8.5.2 以上,主要依赖都支持 K2,那 Kotlin 2.4.0 值得升级。
升级时重点检查这几件事:
- AGP 是否至少 8.5.2
- Gradle 是否在 Kotlin 2.4.0 的兼容范围内
- Compose feature flag 是否还有旧配置
- kapt、KSP、Compose、Hilt、Room、serialization 等编译期工具是否都支持当前 Kotlin 版本