欢迎光临
我们一直在努力

KMP学习day5--Kotlin Flow异步流基础

KMP学习day5–Kotlin Flow异步流基础

在上一节协程入门里,我们用 suspend fun 一次性返回了一个结果。但真实开发中大量数据不是"一个值",而是"一段时间内陆续到来的一串值":传感器每秒上报一次读数、聊天室不断收到新消息、股票行情持续推送。用挂起函数只能反复调用、自己攒列表,写起来笨拙还容易出错。Kotlin 官方为这种场景给出的标准答案就是 Flow——协程世界里的异步数据流。本文把 Flow 的核心概念、常用操作符,以及它和 suspend fun 的边界一次讲清楚。

一、Flow 是什么:冷流与按需生产

Flow 的类型是 Flow<T>,可以理解为"一个最终会陆续产出若干个 T 类型值的异步序列"。它最关键的特性是冷流(cold stream):

  • flow { … } 的代码块在没有收集者(collect)之前一行都不会执行;
  • 每有一个新的收集者,代码块就重新完整执行一遍,各收集者互不影响。

这很像外卖订阅:店铺(Flow 构建器)不会在没人下单时就开始做菜;你(收集者)下单后,菜才按顺序做好、一道道送来;再来一个顾客下单,厨房会重新从头做一份——两个顾客拿到的菜互相独立。冷流保证了"数据是为谁生产的"这个问题始终清晰,不会出现订阅者还没就绪就错过数据的问题。

二、flow 构建器与 collect

创建 Flow 最基础的方式是 flow { } 构建器,在块内用 emit() 发射值,用 collect() 收集:

fun temperatureReadings(): Flow<Int> = flow {
for (temp in listOf(25, 26, 27, 26)) {
delay(100) // 模拟传感器间隔上报
emit(temp) // 发射一个值
}
}

fun main() = runBlocking {
temperatureReadings().collect { value ->
println("收到读数: $value")
}
}

要点:

  • emit 和 collect 都是挂起函数,所以整条流水线天然运行在协程里,不会阻塞线程;
  • Flow 内不允许随意切换协程上下文发射(emit 不是并发安全的),切换线程应交给后面讲的 flowOn,而不是手动 withContext;
  • 除了 flow { },还有便捷构建器:flowOf(1, 2, 3) 快速包装固定值,listOf(…).asFlow() 把已有集合转成流。

三、map、filter:像操作集合一样操作流

Flow 的操作符风格和集合 API 高度一致,map、filter 等都能直接用,区别是这些操作是逐值、异步地作用在流上的:

fun main() = runBlocking {
temperatureReadings()
.filter { it > 25 } // 只要高于 25 度的读数
.map { "警告: 当前 ${it}℃" } // 把读数转成提示文本
.collect { println(it) }
}

这里的每个中间操作符返回的都是新的 Flow,可以像搭流水线一样无限链下去;真正的执行发生在 collect 处——这也是"冷流"语义的体现:声明时只是搭管道,收集时才开闸放水。

当上游发射在需要切换的调度器上(比如 IO 线程读传感器),用 flowOn 指定上游运行上下文,下游保持不变:

temperatureReadings()
.flowOn(Dispatchers.IO) // 上游在 IO 线程生产
.collect { println(it) } // 下游仍在调用者的上下文收集

四、Flow vs suspend fun:一次多个,怎么选

这是初学者最容易纠结的边界问题。判断标准只有一条:要返回的东西是"一个值"还是"一串陆续到来的值"。

场景选择原因
查询一个用户信息 suspend fun 单次请求、单个结果
读取一份配置文件 suspend fun 一次性返回完整数据
传感器持续上报 Flow 值随时间陆续产生,数量不定
数据库表监听(Room) Flow 数据每次变化都要推给 UI
用户输入框实时搜索 Flow 每次输入都是一个新事件

用类比来记:suspend fun 像 EventHub 的单次事件回调——调用一次、回来一次;Flow 则像订阅了一个持续更新的频道——只要订阅着,后续每个事件都会送上门。在 ArkTS/鸿蒙开发里你通常要自己管理回调列表和生命周期,而 Flow 把"订阅、传递、取消"全部纳入结构化并发:收集者所在协程被取消时,Flow 的生产代码会自动随 cancelled 状态终止,不需要手写注销逻辑。

再看一个对照代码:

// suspend 版:只能拿到"截至目前"的一个快照
suspend fun currentTemperature(): Int = ...

// Flow 版:持续收到每一次变化
fun temperatureStream(): Flow<Int> = flow { ... }

五、实际应用与练习

Flow 在 KMP 项目中的典型位置是数据层向表现层推送状态:Room/SQLDelight 的查询监听、网络轮询、WebSocket 消息流,都可以暴露成 Flow,UI 层 collect 后刷新界面,数据层完全不用关心界面是谁。

动手练习(约 15 分钟):模拟一个传感器数据流,综合运用本节内容:

fun sensorStream(): Flow<Int> = flow {
repeat(10) {
delay(100)
emit((0..100).random())
}
}

fun main() = runBlocking {
sensorStream()
.filter { it > 50 } // ① 只保留有效读数
.map { it * 2 } // ② 转换为工程值
.collect { println("有效读数: $it") }

// ③ 冷流验证:再 collect 一次,观察数据是否重新生产
println("—- 第二次收集 —-")
sensorStream().collect { println("再次收到: $it") }
}

运行后会发现第二次 collect 拿到的是另一批全新随机数——这正是冷流"每个收集者独立执行"的直接证据。

最后做一次代码品味挑刺,下面这段埋了 3 处问题的代码:

fun sensorReport(f: Flow<Int>): Int {
var sum = 0
Thread.sleep(1000)
runBlocking { f.collect { sum += it } }
return sum
}

问题在于:① Thread.sleep 是阻塞调用,放进 Flow 相关代码会卡死线程,应该用 delay;② 用 var 手工累加收集结果,正确做法是 f.toList().sum(),意图清晰且可复用;③ sensorReport 命名含糊,看不出是"求和"还是"上报",应改为 sumOfReadings 这类动词明确的名称。

六、小结

  • Flow 是协程生态里表达"异步多值序列"的标准类型,本质是冷流:没有收集者就不生产,每个收集者独立重放。
  • flow { emit() } 构建,collect 收集,map/filter 链式变换,flowOn 切换上游线程。
  • 选型口诀:单个结果用 suspend fun,一串陆续到来的值用 Flow。
  • 取消、生命周期由结构化并发统一托管,这是 Flow 相比手写回调体系的根本优势。

掌握 Flow 基础后,下一站是协程进阶:Dispatcher、异常处理与作用域,届时 Flow 会和这些机制深度配合,处理更复杂的生产环境场景。

赞(0)
未经允许不得转载:171主机测评 » KMP学习day5--Kotlin Flow异步流基础
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址