Kotlin 2.4.0 正式稳定!Android 升级、Compose、KMP 全变化详解

前几天,JetBrains 发布 Kotlin 2.4.0 稳定版。

这次更新里针对Android开发者主要有:语言特性转正、标准库补 API、Gradle/AGP 版本边界、Compose compiler 的增量编译变化。除此之外,KMP、Wasm、JS 也有不少内容。

升级入口

Gradle 项目里,版本号还是从 Kotlin 插件开始改。如果项目已经使用 Compose compiler Gradle plugin,也要一起对齐到同一个 Kotlin 版本。

bash 复制代码
plugins {
    id("org.jetbrains.kotlin.android") version "2.4.0"
    id("org.jetbrains.kotlin.plugin.compose") version "2.4.0"
}

Kotlin 2.4.0 声明兼容 Gradle 7.6.3Gradle 9.5.0。Android 工程还要看一条更直接的限制:最低支持的 Android Gradle Plugin 版本提升到 8.5.2。也就是说,旧项目如果还停在 AGP 8.4 或更早,不能只改 Kotlin 版本。

bash 复制代码
plugins {
    id("com.android.application") version "8.5.2"
    id("org.jetbrains.kotlin.android") version "2.4.0"
}

还有一个容易漏的点:-language-version=1.9 从 2.4.0 开始不再支持,K1 编译器也不再支持。仓库里如果显式写过 languageVersion.set(KotlinVersion.KOTLIN_1_9),或者公共 Gradle convention plugin 里还保留旧配置,升级时会先卡在编译配置上。

稳定特性

2.4.0 把几项之前还在实验阶段的能力转成稳定状态。语言侧包括 context parameters@all property meta-target、注解 use-site target 的新默认规则,以及 explicit backing fields。标准库侧包括 common 里的 kotlin.uuid.Uuid、排序检查 API、JVM 上无符号整数转 BigInteger

context parameters 现在不需要 opt-in,但 context arguments 和 callable references 还不在稳定范围内。这个边界要分清楚,否则很容易以为 context 相关写法全都稳定了。

bash 复制代码
class Logger {
    fun info(message: String) = println(message)
}

context(logger: Logger)
fun trackScreen(screen: String) {
    logger.info("open $screen")
}

显式 context 实参仍然是实验特性,用来处理只靠 context 参数区分重载时的二义性。要试这个写法,需要单独打开编译参数。

bash 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xexplicit-context-arguments")
    }
}

这种能力在普通 Android 页面里不一定马上用上,但在一些 DSL、日志、权限上下文、Repository scope 里会更容易出现。稳定的是基础语法,不代表团队要立刻把现有代码改成 context 风格。

集合字面量

集合字面量是 2.4.0 里比较容易被注意到的新实验特性。打开 -Xcollection-literals 后,可以用 [] 创建集合。

bash 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xcollection-literals")
    }
}

最小写法如下:

bash 复制代码
val names: MutableList<String> = ["Tom", "Jerry", "Spike"]

val tags = ["android", "kotlin", "compose"]
// 没有足够类型信息时,默认推断为 List

这里不是简单把 listOf() 换个符号。编译器会根据期望类型选择具体集合类型,也支持通过 operator fun of 接到自定义类型上。比如矩阵、路由表、测试参数这类 DSL,后面会有更短的写法。

bash 复制代码
class Matrix(vararg val rows: Row) {
    companion object {
        operator fun of(vararg rows: Row) = Matrix(*rows)
    }

    class Row(vararg val values: Double) {
        companion object {
            operator fun of(vararg values: Double) = Row(*values)
        }
    }
}

val identity: Matrix = [
    [1.0, 0.0, 0.0],
    [0.0, 1.0, 0.0],
    [0.0, 0.0, 1.0],
]

限制也要记一下:当前集合字面量不能用来构造 Java 里定义的集合类型。Android 项目里如果只是写业务列表,listOf() 仍然更稳;如果是库作者或 DSL 作者,可以单独开实验模块验证。

标准库补齐

kotlin.uuid.Uuid 进入 common 标准库稳定状态。KMP 项目里,以前如果只是为了 UUID 引入额外库,现在可以先看标准库能不能覆盖。需要注意的是,V4/V7 生成函数还在实验阶段,仍然需要 opt-in。

排序检查 API 也转正了。以前判断列表是否有序,常见写法是自己写循环,或者先 sorted() 再比较。2.4.0 里可以直接用 isSorted()isSortedBy() 这一组 API。

bash 复制代码
data class User(val name: String, val age: Int)

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 遇到第一对逆序元素就会停止,不需要真的构造一个排序后的集合。比如本地缓存、分页数据、服务端返回列表做断言时,用它比 items == items.sortedBy { ... } 更直接。

Map 也补了 nullable value 的 fallback API,不过还是实验状态。它解决的是一个老问题:key 不存在和 key 存在但 value 是 null,以前用 getOrPut() 很容易混在一起。

bash 复制代码
@OptIn(ExperimentalStdlibApi::class)
fun main() {
    val mapForNull = mutableMapOf<String, String?>("user" to null)
    val mapForMissing = mutableMapOf<String, String?>("user" to null)

    mapForNull.getOrPutIfNull("user") { "default_user" }
    mapForMissing.getOrPutIfMissing("user") { "default_user" }

    println(mapForNull)    // {user=default_user}
    println(mapForMissing) // {user=null}
}

这类 API 在缓存里更有用。比如接口查询结果允许为空,空结果本身也要缓存,那就应该区分"没有查过"和"查过但结果为空"。

JVM 和注解

Kotlin/JVM 现在可以生成 Java 26 字节码。Android App 日常不会直接用到 Java 26 target,更多是服务端、桌面或工具链项目会先碰到。

和 Android 生态更相关的是 metadata 里的注解默认开启。编译器会把注解写进 Kotlin metadata,元数据工具、编译期处理器、静态分析工具可以从 metadata 层读取注解信息,不必总是绕到反射或源码解析。

这类变化一般不会要求业务代码改一行,但会影响框架和工具的实现方式。比如做 KSP、API 检查、二进制兼容检查、文档生成时,metadata 里拿到的信息会更完整。

Compose compiler

Kotlin 2.4.0 里的 Compose compiler 有一条 Android 开发者需要看:internal 类型的稳定性推断在增量编译中更一致。

以前跨文件使用 internal class 作为 @Composable 参数时,某些稳定性变化可能不会触发相关使用点重新编译。2.4.0 改成运行时推断这类 internal 类型的稳定性,这样增量编译下的结果更一致。

代价是产物体积可能增加:当 @Composable 函数参数里用了不同文件的 internal class,编译器可能同时编码 stable 和 unstable 两条执行路径。完整应用优化时,R8 能把不需要的路径消掉。

bash 复制代码
// ui/state/UserUiState.kt
internal data class UserUiState(
    val name: String,
    val premium: Boolean,
)

// ui/ProfileCard.kt
@Composable
internal fun ProfileCard(state: UserUiState) {
    Text(text = state.name)
}

这种代码本身不需要改。升级后更应该看的是 release 包体积、R8 是否正常开启,以及 Compose 编译报告里稳定性相关的变化。若项目里还配置了旧 feature flag,也要清掉:StrongSkippingIntrinsicRemember 已经进入 DeprecationLevel.ERROR,后面会移除;OptimizeNonSkippingGroupsPausableComposition 也进入弃用流程。

KMP 变化

Kotlin/Native 这次变化不少。CMS GC 从 2.4.0 开始默认启用,标记阶段可以和应用线程并发执行。KMP 做 iOS UI 或共享业务模块时,这类变化主要影响暂停时间和响应性。如果遇到回归,可以在 gradle.properties 里退回旧策略。

bash 复制代码
kotlin.native.binary.gc=pmcs

Swift export 进入 Alpha,并且补了结构化并发和 Flow 导出到 Swift。suspend 函数可以映射成 Swift 的 async 调用,Flow<T> 可以导出成 AsyncSequence

bash 复制代码
fun flowOfNames(): Flow<String> = flowOf("Alice", "Bob")
bash 复制代码
for try await name in flowOfNames().asAsyncSequence() {
    print(name)
}

KMP iOS 侧还有一个版本边界:Apple target 默认最低版本上调,iOS/tvOS 从 14.0 到 15.0,macOS 从 11.0 到 12.0,watchOS 从 7.0 到 8.0。业务如果还要覆盖更低系统,需要显式覆盖 konan properties。

bash 复制代码
kotlin {
    targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>()
        .configureEach {
            binaries.configureEach {
                freeCompilerArgs += "-Xoverride-konan-properties=minVersion.ios=14.0"
            }
        }
}

另外,Gradle 里可以声明 Swift Package 依赖了。项目原来用 CocoaPods 管 iOS 依赖的,可以单独开分支评估迁移成本。

Wasm 和 JS

Wasm 侧,增量编译稳定并默认开启。遇到问题可以在 gradle.propertieslocal.properties 里关闭。

bash 复制代码
kotlin.incremental.wasm=false

WebAssembly Component Model 进入实验支持。这个方向更偏浏览器外的 Wasm,比如 wasi:http、serverless 场景。Android 项目短期内不会直接受影响,但做 KMP Web 或工具链的人可以跟一下。

JS 侧,@JsExport 可以导出 inline value class 到 TypeScript,js() 内联代码支持 ES2015 语法,包括 constlet、class、generator、箭头函数、展开运算符、模板字符串等。

bash 复制代码
@JsExport
@JvmInline
value class Email(val address: String) {
    init {
        require(address.contains("@")) { "Invalid email" }
    }
}

对写纯 Android App 的团队,这部分不一定要投入时间;对做 KMP SDK、Web 前端互操作、TypeScript 声明导出的团队,2.4.0 的 JS 变化更实用。

最后

都AI时代了,还有人关注Kotlin的新特性吗?

#Kotlin #Android开发 #JetpackCompose #KMP #Gradle

相关推荐
恋猫de小郭2 小时前
Android 官方给 Compose 搞了个不需要 UI 环境的 Composable
android·前端·flutter
珊瑚里的鱼3 小时前
C++的强制类型转换
android·开发语言·c++
问心无愧05133 小时前
ctf show web入门102
android·java·前端·笔记
Kapaseker4 小时前
Kotlin 相等的奥义
android·kotlin
Lyyaoo.4 小时前
【MySQL】锁机制
android·数据库·mysql
DS随心转插件4 小时前
DeepSeek 代码手机端导出与 AI 辅助方案实测
android·人工智能·chatgpt·智能手机·deepseek·ai导出鸭
JohnnyDeng945 小时前
【Android】Flow vs LiveData:选型指南与迁移实践
android·kotlin·livedata·flow
plainGeekDev5 小时前
线程安全集合 → 协程安全替代
android·java·kotlin
zhangphil5 小时前
Kotlin管道Channel构造函数参数capacity值RENDEZVOUS与UNLIMITED
android·kotlin