本文首发于公众号"Android技术圈"
前言
kotlinx.coroutines 1.11.0 发布了。
这一版最直接的变化是 Kotlin 升到 2.2.20 ,同时新增了一些 Flow / StateFlow API,并修复 SharedFlow、flowOn、RxJava 互操作、R8 GC、JS/Wasm 异常处理相关问题。

Flow 和 StateFlow 推出新 API
这次有一组新的 Flow / StateFlow API,比较值得 Android 开发者注意的是这几个:
-
•
SharedFlow.asFlow():把SharedFlow暴露成普通Flow视图,隐藏"这是热流"的实现细节,目前还是ExperimentalCoroutinesApi -
•
StateFlow.onSubscription {}:给StateFlow增加订阅回调,并且返回值仍然是StateFlow -
•
StateFlow.collectLatest {}的新重载:返回Nothing,帮助编译器识别后面的代码不可达 -
•
Flow.associate/associateBy/associateWith/...To:直接把 Flow 收集成Map
如果你不想让外部代码依赖 StateFlow 的"当前值"和热流语义,1.11.0 以后可以显式把它收窄成普通 Flow:
bash
class DeviceRepository {
private val _devices = MutableStateFlow<List<Device>>(emptyList())
val devices: Flow<List<Device>> = _devices.asFlow()
}
这不是把热流变成冷流。它的作用是收窄类型:调用方只能按 Flow 收集,不能读取 .value,也不应该关心内部到底是 StateFlow、SharedFlow,还是以后换成别的实现。
StateFlow.onSubscription 则适合放订阅侧的轻量动作,例如页面第一次开始观察时触发刷新:
bash
val uiState: StateFlow<DeviceUiState> =
repository.deviceState
.onSubscription {
refreshDevice()
}
这里的重点是返回值仍然是 StateFlow<DeviceUiState>。以前你在 StateFlow 上接普通 Flow 操作符,很容易把类型变成 Flow,后续如果还要保留状态语义,就得额外封装。
新的 Map 终止操作符则更像补齐集合 API 的体验。以前要把一次性 Flow 结果整理成索引 Map,常见写法是先 toList() 再 associateBy():
bash
val devicesById: Map<String, Device> =
repository.loadDevices()
.associateBy { device -> device.id }
这类 API 和 stateIn、shareIn 不是一个层面的东西:它不会改变共享、订阅、replay 这些语义,只是让"收集完成后变成 Map"的终止操作更直接。

SharedFlow 修复更贴近事件流
SharedFlow 经常被用来做一次性事件。
比如 Toast、导航、弹窗、Snackbar、蓝牙扫描结果、设备连接状态变化:
bash
private val _events = MutableSharedFlow<DeviceEvent>(
replay = 0,
extraBufferCapacity = 1
)
val events: SharedFlow<DeviceEvent> = _events
fun onConnectFailed(reason: String) {
_events.tryEmit(DeviceEvent.ShowError(reason))
}
SharedFlow 的坑一般出现在边界上:有没有订阅者、buffer 满了怎么办、replay 是否会把旧事件重新发给新页面、取消时是否还有悬挂 collector。
这次 release 明确提到修复 SharedFlow 相关问题。对应用开发者来说,最应该回归的是三类场景。
第一类是页面重建。
Android 上旋转屏幕、进后台再回来、Compose navigation 切换页面,都可能让 collector 重新订阅。如果事件流设置不合理,用户会看到旧 Toast 重放、旧导航再次触发。
第二类是订阅时序。
事件先发出,collector 后启动,replay = 0 时事件本来就可能丢;如果业务不能接受丢事件,就不应该用裸 SharedFlow 当可靠消息队列。
第三类是高频事件。
扫描、传感器、日志、下载进度这类流量大,extraBufferCapacity 和 overflow 策略会直接影响内存和丢弃行为。
升级后建议把 SharedFlow 用法扫一遍:
bash
MutableSharedFlow<Event>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
这段配置不是模板。它只说明三个参数必须一起看:是否重放、额外缓存多大、满了以后丢旧值还是挂起发送者。
flowOn 修复影响线程边界
flowOn 是 Flow 里最容易被误解的 API 之一。
它改变的是上游执行上下文,不是整条链路的所有代码。
bash
val flow = flow {
emit(api.fetchUser())
}
.map { user -> user.toUiModel() }
.flowOn(Dispatchers.IO)
.onEach { ui ->
render(ui)
}
这里 flow {} 和 map 在 Dispatchers.IO,onEach 在 collector 所在上下文。
flowOn 相关 bug 修复对真实项目有影响,因为很多项目会把网络、数据库、文件读取包进 Flow,再通过 flowOn(Dispatchers.IO) 切线程。
升级后重点回归这些位置:
-
•
flowOn前后有没有依赖 ThreadLocal / MDC / tracing context -
• 上游异常是否还能被下游
catch捕获 -
• collector 取消后,上游 IO 是否及时停止
-
• 多个
flowOn叠加时,线程是否符合预期
不要在 Flow 里同时到处写 withContext(Dispatchers.IO) 和 flowOn(Dispatchers.IO)。它们表达的边界不同,混用会让代码难测。
更清晰的写法是把数据源放在上游,然后让 flowOn 明确标出上游线程:
bash
fun observeUser(): Flow<User> {
return flow {
emit(userApi.fetchUser())
emitAll(userDao.observeUser())
}.flowOn(Dispatchers.IO)
}
UI 层只关心收集,不要再猜数据源在哪个线程执行。

最后
kotlinx.coroutines 1.11.0 不是只给 Kotlin 2.2.20 换个适配版本。
它更像一次底层稳定性更新:Flow / StateFlow 继续补 API,SharedFlow、flowOn、RxJava、R8、JS/Wasm 这些真实工程里的边界问题被继续修掉。