如果你熟悉 RxJava,那么 Kotlin Flow 就是 Kotlin 协程世界的 RxJava;如果你不熟悉,没关系,你可以把 Flow 想象成一条可以暂停、恢复的"数据流水线"。
1. 为什么需要 Flow?
在 Flow 出现之前,我们处理多个连续数据(比如每秒更新一次的倒计时,或者数据库的实时变化)通常很麻烦:
- List/Sequence: 只能一次性返回所有数据,不能异步返回(会阻塞)。
- LiveData: 只能在主线程运行,操作符支持有限,且主要用于 UI 层。
- RxJava: 功能强大但学习曲线陡峭,且容易造成"回调地狱"。
Flow 是基于 Kotlin 协程 构建的,它天生支持挂起(Suspend),这意味着它可以在不阻塞线程的情况下,异步地生成和消费数据。
2. 流水线三部曲
使用 Flow 就像搭建一条工厂流水线:
- 上游 (Upstream) : 生产者,负责制造数据(发射
emit)。 - 中游 (Intermediaries) : 工人,负责加工数据(操作符
map,filter等)。 - 下游 (Downstream) : 消费者,负责接收成品(收集
collect)。
2.1 最简单的例子
Kotlin
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
// 1. 上游:创建一个 Flow
flow {
println("开始生产...")
emit(1) // 发射数据
delay(100) // 模拟耗时,不会阻塞线程
emit(2)
emit(3)
}
// 2. 中游:操作符处理
.filter { it % 2 != 0 } // 只保留奇数
.map { "数字: $it" } // 转换成字符串
// 3. 下游:收集数据(末端操作符)
.collect { value ->
println("收到: $value")
}
}
输出:
Kotlin
开始生产...
收到: 数字: 1
收到: 数字: 3
3. 冷流 vs 热流 (Cold vs Hot)
这是面试和实战中最关键的概念。
3.1 普通 Flow 是"冷"的 (Cold)
特点 :只有当你调用 collect() 时,上游的代码才会执行。如果不收集,它就什么都不做。
比喻:像一张 CD 唱片。你不播放(collect),音乐就不会响;你每播放一次,音乐就从头开始放一遍。
3.2 StateFlow / SharedFlow 是"热"的 (Hot)
特点 :无论有没有人收集,它都在那里。通常用于状态管理 。
比喻:像直播电台。不管有没有听众,电台都在广播;你晚进来了,就只能听到之后的内容(或者最新的那一条)。
StateFlow (状态流)
- 用途 :专门用于替代
LiveData,存储 UI 状态(如UiState)。 - 特性 :粘性 (Sticky)。新订阅者会立即收到当前最新的值。
- 去重:如果新设置的值和旧值一样,不会触发更新。
Kotlin
// ViewModel 中
private val _uiState = MutableStateFlow(LoginState()) // 必须有初始值
val uiState = _uiState.asStateFlow()
// 更新状态
_uiState.value = newState
// 或者
_uiState.update { it.copy(isLoading = true) }
SharedFlow (共享流)
- 用途:用于一次性事件(如 Toast、导航、SnackBar)。
- 特性 :默认非粘性。事件发出去,如果当时没人在听,这事就过去了。
Kotlin
// ViewModel 中
private val _events = MutableSharedFlow<String>()
val events = _events.asSharedFlow()
// 发送事件
viewModelScope.launch {
_events.emit("登录成功")
}
4. 常用操作符实战
Flow 的强大在于丰富的操作符。
4.1 线程切换:flowOn
RxJava 里的 subscribeOn,在 Flow 里极其简单。
Kotlin
flow {
// 这里在 IO 线程执行(如下载文件)
emit(downloadFile())
}
.flowOn(Dispatchers.IO) // 指定上游在 IO 线程
.collect {
// 这里在主线程执行(如果是在 UI 作用域启动的)
updateUi(it)
}
4.2 异常捕获:catch
不要用 try-catch 包裹 collect,而应该使用声明式的 catch 操作符。
Kotlin
flow {
emit(1)
throw RuntimeException("出错啦")
}
.catch { e ->
emit(-1) // 发生错误时发射一个兜底值
println("捕获到异常: ${e.message}")
}
.collect { println(it) }
4.3 UI 防抖:debounce
防止用户疯狂点击搜索按钮,造成网络拥堵。
Kotlin
searchQueryFlow
.debounce(300) // 300ms 内没有新输入,才发送请求
.flatMapLatest { query ->
// 自动取消旧的搜索请求,只处理最新的
api.search(query)
}
.collect { results -> show(results) }
5. Android / Compose 中的最佳实践
在 Android UI 中收集 Flow 时,必须注意生命周期安全。否则,当应用退到后台时,Flow 还在后台一直更新 UI,会导致崩溃或耗电。
在 Compose 中(推荐)
使用 collectAsStateWithLifecycle(需要引入 androidx.lifecycle:lifecycle-runtime-compose 库)。
Kotlin
@Composable
fun UserScreen(viewModel: UserViewModel) {
// 自动感知生命周期:APP 切后台时暂停收集,切回来恢复
val state by viewModel.uiState.collectAsStateWithLifecycle()
if (state.isLoading) {
LoadingView()
} else {
ContentView(state.data)
}
}
在传统 View 系统中
使用 repeatOnLifecycle。
Kotlin
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
// 更新 View
}
}
}
总结
- Flow 是处理异步数据流的利器,支持挂起函数。
- Cold Flow (普通 Flow)像 CD,按需启动;Hot Flow(StateFlow)像直播,状态常驻。
- ViewModel 中使用
StateFlow管理状态,SharedFlow管理事件。 - UI 层 收集时务必使用
collectAsStateWithLifecycle或repeatOnLifecycle保证安全。