Flow
今日核心目标
- 掌握Flow基础核心用法、callbackFlow/channelFlow实战技巧,解决数据监听、回调嵌套痛点。
- 掌握Flow进阶用法(节流、防抖、StateFlow/SharedFlow),解决高频数据发射、多页面共享数据的痛点。
- 将Flow与协程无缝衔接,形成完整的异步处理+状态管理体系,进一步提升代码性能和工程化水平。
Flow知识点
- Flow基础核心
- Flow核心操作符
- callbackFlow与channelFlow实战
Flow基础核心
- 定义:Flow是Kotlin用于处理异步数据流的工具,基于协程实现,支持冷流特性(只有被收集时才发射数据)。适合处理连续的异步数据,替代Callback解决回调嵌套问题。
- 例:蓝牙实时数据、传感器数据。
- 优势:相比Callback,代码简洁、可读性强,支持链式调用,且与协程无缝联动。适合处理连续数据发射场景。
- 例:蓝牙持续接收数据。
- 用法:通过
flow构建数据流,用emit发射数据。通过collect收集数据,收集过程会挂起协程,不阻塞主线程,适配Android UI操作和耗时操作场景。
Flow核心操作符
- map:数据转换。适合将原始字节数据转换为实体类(例:网络请求数据、蓝牙数据解析)。
- 不改变数据流的发射节奏
- filter:数据过滤。适合过滤无效数据,只保留符合条件的数据。
- 例:过滤蓝牙信号强度低于-80dBm的设备数据
- catch:异常捕获。结合异常处理器,捕获Flow发射、处理过程中的异常,避免App闪退。
- 例:网络请求异常、蓝牙中断导致数据发射失败
- collectLatest:最新值收集。适合数据实时更新场景,提升性能。
- 例:蓝牙信号强度实时变化,只收集最新的信号值,取消之前未处理的收集任务
callbackFlow与channelFlow实战
- callbackFlow:专门用于桥接传统回调式API,将回调转换为Flow数据流,自动处理资源清理,避免回调嵌套。
- 通过
callbackFlow{}构建,用trySend()非阻塞发射数据 - 通过
awaitClose()在Flow取消时清理资源- 例:取消网络请求、注销蓝牙回调
- 通过
- channelFlow:用于从多个协程并发发射数据,支持跨协程发射。
- 适合多设备蓝牙数据并发接收、单页面多模块数据并发请求场景
- 可整合多个异步数据源,通过send()发射数据,
awaitClose()等待所有发射完成并清理资源
callbackFlow与channelFlow区别
- callbackFlow:侧重回调转Flow,解决回调嵌套。
- channelFlow:侧重并发数据源整合,解决多协程并发发射数据问题。
蓝牙开发中的使用
- 用
callbackFlow包装蓝牙Gatt回调,将蓝牙连接状态、数据接收转换为Flow - 用
channelFlow整合多个蓝牙设备的数据流,实现并发处理,提升效率。
Flow进阶用法
- Flow节流与防抖(解决高频数据痛点)
- StateFlow与SharedFlow(解决多页面数据共享)
- Flow内存泄漏防控(规避线上问题)
Flow节流与防抖(解决高频数据痛点)
- 场景
- 蓝牙设备会高频发射数据(例:蓝牙音频高频传输、传感器数据、实时信号强度)。
- NDK多任务并行解析会高频返回结果。
- 若不做控制,会导致UI频繁刷新、数据处理卡顿、资源浪费,甚至引发ANR,节流与防抖即为解决方案。
- 节流(throttleLatest/sample):控制数据处理频率,在指定时间内只处理一次数据(或只取最新数据)。
- 适合蓝牙高频数据监听场景(例:每100ms只处理一次蓝牙信号强度,避免UI频繁刷新)。
- throttleLatest:在指定时间窗口内取最新值。
- sample:在指定时间窗口内只取一次值。
- 适合蓝牙高频数据监听场景(例:每100ms只处理一次蓝牙信号强度,避免UI频繁刷新)。
- 防抖:等待数据稳定后再处理,若在指定时间内有新数据发射,则重新计时。
- 适用于蓝牙设备连接状态频繁切换、NDK解析结果频繁返回的场景(例:蓝牙连接时"连接中-断开-连接中"的频繁切换,防抖后只处理最终稳定的连接状态)。
- 坑点:节流和防抖误用
- 蓝牙数据接收场景用防抖,导致关键数据丢失。
- 连接状态切换用节流,导致状态更新不及时。
- 未结合协程线程池,导致节流防抖操作阻塞主线程。
kotlin
/**
* 节流:指定时间内取最新值,适合高频数据监听
*/
fun <T> Flow<T>.throttleLatest(milliseconds: Long): Flow<T> = this
.throttleLatest(milliseconds)
.flowOn(multiTaskDispatcher) // 绑定协程池,避免阻塞主线程
.catch { throwable ->
coroutineHandler.handleException(multiTaskDispatcher, throwable)
}
/**
* 节流:指定时间内取一次值,适合高频数据
*/
fun <T> Flow<T>.sample(milliseconds: Long): Flow<T> = this
.sample(milliseconds)
.flowOn(multiTaskDispatcher) // 绑定协程池,避免阻塞主线程
.catch { throwable ->
coroutineHandler.handleException(multiTaskDispatcher, throwable)
}
/**
* 防抖:等待数据稳定后处理
*
* 适合蓝牙连接状态、NDK解析结果
*/
fun <T> Flow<T>.debounce(milliseconds: Long): Flow<T> = this
.debounce(milliseconds)
.flowOn(multiTaskDispatcher) // 绑定协程池,避免阻塞主线程
.catch { throwable ->
// 结合全局异常处理,避免闪退
coroutineHandler.handleException(multiTaskDispatcher, throwable)
}
StateFlow与SharedFlow(解决多页面数据共享)
- 定位:二者均是Kotlin Flow的热流,无论是否被收集,都会发射数据,持有数据状态。
- 区别于Flow冷流(只有被收集才发射数据)。
- 专门用于多页面数据共享、状态管理。
- StateFlow:状态容器,必须有初始值,始终持有最新的单个状态值。
- 新观察者注册时会立即收到当前最新值,适合管理单一状态
- 例:网络请求状态、蓝牙全局连接状态。
- 状态管理的首选,贴合蓝牙全局状态同步场景。
- 默认配置:
replay=1仅缓存最新值、无额外缓冲区,数据更新采用覆盖式,新值会替换旧值。
- 新观察者注册时会立即收到当前最新值,适合管理单一状态
- SharedFlow:通用共享数据流,可选初始值。
- 可配置缓存策略
replay缓存数量、缓冲区大小。 - 适合分发一系列事件或多源数据
- 例:单页面多模块数据请求、蓝牙多设备数据、NDK多任务解析日志。
- 新观察者默认只收到注册后的新数据(可通过replay配置回溯历史数据)。
- 适合多页面共享连续数据流。
- 可配置缓存策略
StateFlow与SharedFlow区别
- StateFlow
- 侧重单一状态管理,让所有页面都能实时获取最新状态
- 例:网络请求状态、蓝牙是否连接、NDK解析是否完成。
- 必须有初始值
- 新观察者能拿到最新值
- 侧重单一状态管理,让所有页面都能实时获取最新状态
- SharedFlow
- 侧重多源事件分享
- 例:蓝牙多设备数据、NDK解析日志流。
- 初始值可选,新观察者默认不能收到最新值(需配置replay)
- 分发蓝牙多设备实时数据、NDK多任务解析结果,实现多页面同步接收数据,解决多页面数据共享频繁、状态不一致问题。
- 侧重多源事件分享
Flow内存泄漏防控(规避线上问题)
- 泄露场景
- 用GlobalScope收集Flow
- Flow收集未与页面生命周期绑定
- StateFlow/SharedFlow未及时取消收集
- 持有Activity/Fragment引用导致无法回收
- 防控方案
- 使用lifecycleScope、viewModelScope收集Flow
- 确保页面销毁、ViewModel清除时,Flow收集自动取消
- 避免在Flow中持有Activity/Fragment的强引用,可用弱引用或通过状态流间接更新UI
- StateFlow/SharedFlow需合理管理生命周期,避免长期持有不必要的引用
适配
将Flow收集与页面生命周期、ViewModel生命周期绑定。封装形成数据处理+异常捕获+内存防控体系。
kotlin
/**
* Flow与协程联动:页面绑定生命周期,避免泄漏
*/
fun <T> Flow<T>.bindLifecycle(activity: Activity): Flow<T> = this
.onStart {
// 页面启动时收集
}
.onCompletion {
// 页面销毁时取消收集
if (activity.isDestroyed) {
currentCoroutineContext().cancel(CancellationException("$activity isDestroyed"))
}
}
/**
* 观察者绑定生命周期
*/
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
flowRepository.getTestsFlow()
.catch { e ->
LogTool.e("MainActivity", "callbackFlow error: ${e.message}")
}
.collect { tests ->
LogTool.i("MainActivity", "callbackFlow tests: $tests")
}
}
}