文章目录
- [第 1 章 从回调到响应式流:冷流与热流](#第 1 章 从回调到响应式流:冷流与热流)
- [第 2 章 Flow 构建:flow、flowOf 与 asFlow](#第 2 章 Flow 构建:flow、flowOf 与 asFlow)
- [第 3 章 算子链:map、filter 与不可变变换](#第 3 章 算子链:map、filter 与不可变变换)
- [第 4 章 冷转热:stateIn 与 shareIn](#第 4 章 冷转热:stateIn 与 shareIn)
- [第 5 章 StateFlow 与 MutableStateFlow:ViewModel 状态封装](#第 5 章 StateFlow 与 MutableStateFlow:ViewModel 状态封装)
- [第 6 章 UI 收集:repeatOnLifecycle 与单向数据流](#第 6 章 UI 收集:repeatOnLifecycle 与单向数据流)
- [面试速查 · 追问链](#面试速查 · 追问链)
-
-
- [追问链 #1:Flow 为什么是冷流?和 StateFlow 怎么区分? 🔥](#1:Flow 为什么是冷流?和 StateFlow 怎么区分? 🔥)
- [追问链 #2:stateIn 的 WhileSubscribed 解决什么问题? 🔥](#2:stateIn 的 WhileSubscribed 解决什么问题? 🔥)
- [追问链 #3:StateFlow 和 LiveData 新代码怎么选? 🔥](#3:StateFlow 和 LiveData 新代码怎么选? 🔥)
- [追问链 #4:repeatOnLifecycle 解决什么问题? ⭐](#4:repeatOnLifecycle 解决什么问题? ⭐)
- [追问链 #5:map 算子里的异常怎么处理? 💡](#5:map 算子里的异常怎么处理? 💡)
-
- 完整链路一句通
- 相关推荐
第 1 章 从回调到响应式流:冷流与热流
协程入门后,Repository 已能用 suspend 拉一次数据。但 Android 里还有三类需求 suspend 单独扛不住:
- 持续变化:收藏列表、搜索建议、Room 表观察------数据会多次推送,不是「请求一次返回一次」。
- 多步变换 :原始 DTO 要经过
map、过滤、合并多个数据源,再交给 UI。 - 统一状态出口 :加载中、列表、错误应合成一个
UiState,而不是三个独立字段各自postValue。
Flow 是协程世界的异步数据流 API,按「拉取」模型工作:下游 collect 才驱动上游执行。这种叫冷流------没有收集者,上游代码根本不跑。
StateFlow / SharedFlow 是热流 :始终有独立上游(或缓存值),新订阅者立刻拿到当前态或按 replay 配置重放。MutableStateFlow 缓存 UI 状态就是典型热流。
| 维度 | 冷流 Flow |
热流 StateFlow |
|---|---|---|
| 何时执行 | 有 collect 才执行 |
与收集者无关,可早已有值 |
| 多收集者 | 各跑一份上游 | 共享同一数据源 |
| 典型场景 | Repository 拉接口、Room 观察 | ViewModel UiState |
| Android 选型 | 数据管道 | 屏幕「当前长什么样」 |
工作区工程里,UserLocalRepository.favoritesFlow() 用 flow { emit(...) } 暴露收藏列表------只有有人 collect 才会执行 getFavoriteSceneries()(冷流特征):
65:67:app/src/main/kotlin/com/kuen/beautifulchina/data/repository/UserLocalRepository.kt
fun favoritesFlow(): Flow<List<Scenery>> = flow {
emit(getFavoriteSceneries())
}
而 ExploreViewModel 用 MutableStateFlow 持有 ExploreUiState------不 collect 也有当前值,旋转屏后 Fragment 重新订阅能立刻拿到最新列表(热流特征,详见第 5 章)。
原理补充
冷流类似 Sequence,但支持挂起与背压;热流继承 SharedFlow 基础设施,StateFlow 要求必有初始值 ,用 CAS 更新,保证 value 始终可读。混淆冷/热会导致:无 UI 时白跑网络(误把冷流当热流共享),或多次重复请求(该 stateIn 却没共享)。
踩坑
- 把
flow { api.fetch() }当全局事件总线,多处collect重复打接口。 - 用
StateFlow表达「导航到详情」一次性事件------旋转屏会重放。 - 认为「用了 Flow 就不会重复执行」------冷流每个收集者各执行一遍。
动手练
- A :用一句话区分「Room 返回的
Flow<List<Entity>>」与「ViewModel 的StateFlow<UiState>」冷热属性。 - D :在工作区搜索
flow {与MutableStateFlow,各找 1 处并判断冷/热。
第 2 章 Flow 构建:flow、flowOf 与 asFlow
标准库提供三类常用构建方式,按数据来源选型:
| 构建器 | 用途 |
|---|---|
flow { } |
挂起块内 emit,适合调 Repository、读库、轮询 |
flowOf(a, b, c) |
已知有限序列,测试与原型 |
list.asFlow() |
集合转流,衔接集合 API |
flow 构建器内可调用 suspend 函数,这是与 RxJava Observable.create 的根本差异------上游逻辑直接写在协程上下文里:
kotlin
fun searchSceneries(query: String): Flow<List<Scenery>> = flow {
if (query.isBlank()) {
emit(emptyList())
return@flow
}
emit(repository.search(query))
}
工作区 favoritesFlow() 是最小冷流 :collect 一次 emit 一次。若收藏表会频繁变化,更现代的做法是让 DAO 返回 Flow(Room 观察)或在 flow 内 while 轮询------本篇重点在机制,工程上优先 Room Flow。
测试里用 flowOf 快速构造假数据:
kotlin
@Test
fun `filter empty query`() = runTest {
val results = mutableListOf<List<Scenery>>()
searchSceneries("").toList(results)
assertEquals(listOf(emptyList()), results)
}
asFlow() 适合已有 List 想接入算子链:
kotlin
val ids = listOf("a", "b", "c")
ids.asFlow()
.map { repository.getSceneryById(it) }
.filterNotNull()
.collect { scenery -> /* ... */ }
原理补充
flow { } 体在 FlowCollector 上下文执行;emit 是挂起点,遵守结构化并发。flowOf/asFlow 实现为固定上游,同样冷流语义。flowOn(Dispatchers.IO) 切换的是该操作符上游的调度器,不改变冷流本质。
踩坑
- 在
flow { }里用launch异步emit却不等待------竞态与丢事件。 flowOf模拟网络延迟却不包runTest------单元测试挂起超时。- 忘记
flow块内抛异常会向下游传播为catch可捕获的失败。
动手练
- A :把
suspend fun load(): List<Scenery>改写成fun loadFlow(): Flow<List<Scenery>>(3 行)。 - B(起点) :下面在
flow里开了子协程,说明问题并改为顺序emit:
kotlin
fun broken(): Flow<Int> = flow {
launch { emit(1) }
emit(2)
}
第 3 章 算子链:map、filter 与不可变变换
Flow 的价值在于声明式管道 :Repository 输出原始数据,ViewModel 用算子映射为 UI 可消费形态,避免在 collect 里堆业务逻辑。
常用算子:
| 算子 | 作用 | 典型场景 |
|---|---|---|
map |
一对一变换 | Dto → Domain、List → UiModel |
filter / filterNotNull |
丢弃元素 | 只要已发布项 |
distinctUntilChanged |
相邻重复跳过 | 避免相同 UiState 重复刷新 |
catch |
捕获上游异常 | 映射为 Result 或默认空列表 |
onEach |
副作用窥探 | 日志(生产慎用) |
Repository 层清洗示例:
kotlin
fun observeFavorites(): Flow<List<SceneryCardUi>> =
userLocalRepository.favoritesFlow()
.map { list -> list.map { it.toCardUi() } }
.catch { e ->
logger.warn("favorites failed", e)
emit(emptyList())
}
.flowOn(Dispatchers.IO)
ViewModel 侧常把冷流末端转为 UI 态:
kotlin
repository.observeFavorites()
.map { cards ->
ExploreUiState(sceneries = cards, isLoading = false)
}
distinctUntilChanged() 对 data class UiState 尤其有用------copy 后若字段相等,下游可跳过渲染。注意:List 引用相等性;列表内容变了但复用同一 List 实例时 distinctUntilChanged 可能失效,应保证不可变列表(每次 copy 新列表)。
原理补充
算子返回的仍是冷流 (除非后面接 stateIn)。每个算子包装上游,collect 时从下到上建立调用链。map 是 transform 的简化;异常在 map 内抛出会终止流,除非 catch 恢复。
踩坑
- 在
map里做重量级 IO 却忘记上游flowOn(IO)------阻塞默认调度器。 filter后类型收窄,仍用旧类型访问字段------注意 Kotlin 智能转换在 lambda 内的限制。- 链过长却不
stateIn共享------两个 Fragment collect 同一 Repository 冷流会双倍 IO。
动手练
- C :为
favoritesFlow()加map+catch,错误时 emit 空列表(≤6 行)。 - A :
distinctUntilChanged与distinctUntilChangedBy { it.id }使用场景各一句。
第 4 章 冷转热:stateIn 与 shareIn
冷流每个收集者各跑一遍上游。多界面共享同一网络/数据库管道,或配置变更后重新 collect,应在合适层转为热流。
| API | 产出 | 必备参数 | 典型用途 |
|---|---|---|---|
stateIn |
StateFlow |
initialValue、SharingStarted |
UI 状态、需当前值 |
shareIn |
SharedFlow |
SharingStarted、replay |
事件总线、无初值共享 |
ViewModel 内把 Repository 冷流转为 StateFlow:
kotlin
val favoritesUiState: StateFlow<ExploreUiState> =
repository.observeFavorites()
.map { list -> ExploreUiState(sceneries = list, isLoading = false) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ExploreUiState(isLoading = true),
)
SharingStarted.WhileSubscribed(5_000) 含义:有订阅者时启动上游;最后一个订阅者消失后延迟 5 秒 再停------避免旋转屏瞬间停流又重启。Eagerly 则无论有无订阅都跑(慎用耗电)。
shareIn 用于无状态初值的共享,例如日志流:
kotlin
private val _events = MutableSharedFlow<UiEvent>(extraBufferCapacity = 1)
val events = _events.asSharedFlow()
一次性导航事件用 SharedFlow(replay=0) 或 Channel,不要 stateIn------状态流会重放最后一次导航。
工作区 ExploreViewModel 当前用手动 MutableStateFlow + viewModelScope.launch 更新,与 stateIn 等价于「自己管热状态」。当数据源已是 Room Flow 时,优先 stateIn 减少样板代码。
原理补充
stateIn/shareIn 在 scope 内启动共享协程,上游只执行一份。WhileSubscribed 通过订阅计数控制启停,是 Android 省电关键。stateIn 的 initialValue 在首个真实值到来前供 UI 显示占位(Loading)。
踩坑
stateIn的scope用GlobalScope→ 泄漏与无法取消。WhileSubscribed(0)旋转屏立即停上游 → 频繁重启、列表闪动。- 把
shareIn当StateFlow用却不设replay→ 新订阅者收不到历史。
动手练
- A :解释
WhileSubscribed(5_000)与Eagerly在电量上的差异。 - B(起点) :两处 Fragment 各自
collect同一repository.observeFavorites()冷流,如何用stateIn合并为一份(ViewModel 骨架 ≤10 行)?
第 5 章 StateFlow 与 MutableStateFlow:ViewModel 状态封装
新工程 UI 状态默认 StateFlow ,而非 LiveData 多字段或 Java 式回调。核心约定:私有可变、公开只读 ,更新用 update 或 value = 整体替换。
工作区 ExploreViewModel 是标准模板------ExploreUiState 为 data class,copy 驱动不可变迁移:
17:20:app/src/main/kotlin/com/kuen/beautifulchina/ui/viewmodel/ExploreViewModel.kt
data class ExploreUiState(
val sceneries: List<Scenery> = emptyList(),
val isLoading: Boolean = false,
)
32:48:app/src/main/kotlin/com/kuen/beautifulchina/ui/viewmodel/ExploreViewModel.kt
private val _uiState = MutableStateFlow(ExploreUiState())
val uiState: StateFlow<ExploreUiState> = _uiState.asStateFlow()
init {
refresh()
}
fun refresh() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
val list = if (category != null) {
repository.getSceneriesByCategory(category)
} else {
repository.getAllSceneries()
}
_uiState.update { it.copy(sceneries = list, isLoading = false) }
}
}
链路解读:
_uiState私有,外部只能读uiState。refresh()先copy(isLoading = true),UI collect 到后显示进度。- Repository 返回后再次
copy,列表与 loading 一次更新,避免多字段不同步。 viewModelScope保证 ViewModel 销毁时取消协程。
对比 LiveData 遗留写法(维护存量时可对照,新代码不用):
| 维度 | LiveData | StateFlow |
|---|---|---|
| 初值 | 可空、无强制 | 必有当前值 |
| 与 Flow 组合 | 需 asFlow() 桥接 |
原生算子链 |
| 生命周期安全 | 内建 LifecycleOwner |
需 repeatOnLifecycle |
| 新模块默认 | 否 | 是 |
复杂屏幕可把 ExploreUiState 演进为 sealed interface(Loading / Success / Error),但「单 data class + 布尔字段」对列表页已足够,与工程现状一致。
原理补充
StateFlow 要求相等性不变才通知收集者(distinctUntilChanged 语义内建)。update { } 原子读-改-写,避免并发下丢更新。asStateFlow() 返回只读视图,编译期阻止外部 value =。
踩坑
- 对外暴露
MutableStateFlow→ UI 层可绕过 ViewModel 改状态,破坏 UDF。 copy时复用可变List并add→ 相同引用StateFlow可能不发射。- 在
init里launch却不处理CancellationException→ 取消被当错误。
动手练
- A :默写
_uiState/uiState/asStateFlow()三行模板。 - B(起点) :合并分散字段为单一
ExploreUiState并改写refresh()(参考上文引用块)。
第 6 章 UI 收集:repeatOnLifecycle 与单向数据流
StateFlow 不会自动感知 Fragment 生命周期------STOPPED 时仍 collect 会浪费 CPU,且可能在后台改 UI。View 体系统一用 repeatOnLifecycle(Lifecycle.State.STARTED) 包 collect;Compose 用 collectAsStateWithLifecycle()(本质相同,本篇预览概念)。
工作区 ExploreFragment 收集 uiState 并驱动列表与加载指示器:
67:79:app/src/main/kotlin/com/kuen/beautifulchina/ui/fragment/ExploreFragment.kt
private fun observeViewModel() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
adapter.updateList(state.sceneries)
binding.loadingProgress.visibility =
if (state.isLoading) View.VISIBLE else View.GONE
if (!state.isLoading) {
binding.swipeRefresh.isRefreshing = false
}
}
}
}
}
ProvinceDetailFragment 同一模式,可空字段在 UI 层用 ?.let 收窄:
105:113:app/src/main/kotlin/com/kuen/beautifulchina/ui/fragment/ProvinceDetailFragment.kt
private fun observeViewModel() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
state.province?.let { bindProvince(it) }
sceneryAdapter.updateList(state.sceneries)
}
}
}
}
UDF 闭环:
text
用户操作(下拉刷新)→ viewModel.refresh()
↓
ViewModel 更新 StateFlow
↓
repeatOnLifecycle(STARTED) 内 collect
↓
Adapter / View 渲染(只读 state,不反向改 ViewModel)
collect vs collectLatest:搜索框防抖常用 collectLatest(新值取消旧处理);写库、提交表单用 collect 保证每次处理完。列表页展示 UiState 通常 collect 即可。
Compose 预览(新界面默认):
kotlin
@Composable
fun ExploreScreen(viewModel: ExploreViewModel) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
if (state.isLoading) CircularProgressIndicator()
else SceneryGrid(state.sceneries)
}
原理补充
repeatOnLifecycle 在状态低于 STARTED 时取消 块内协程,块内 collect 随之挂起取消;重新 STARTED 时重新执行块,因此会再次收到 StateFlow 当前值。不要用 lifecycleScope.launch { collect } 无边界收集。
踩坑
- 在
onCreate用lifecycleScope而非viewLifecycleOwner收集 →FragmentView 销毁后仍更新 binding。 collect里启动新launch不随生命周期取消 → 泄漏。- 把导航副作用放在
collect里不设Consumed标记 → 旋转重复导航(应走SharedFlow事件通道)。
动手练
- C :为
ExploreFragment的observeViewModel加collectLatest实验,说明与当前collect的差异(注释即可)。 - D 验收清单:旋转屏 + 切后台再回前台,确认列表与 loading 不错乱、无重复请求异常日志。

面试速查 · 追问链
追问链 #1:Flow 为什么是冷流?和 StateFlow 怎么区分? 🔥
标准回答 (≤200 字):Flow 只有下游 collect 时上游 flow {} 才执行,每个收集者独立跑一份,类似挂起版 Sequence。StateFlow 是热流,始终有当前值,多收集者共享。Repository 暴露冷 Flow 拉数据;ViewModel 用 StateFlow 持 UiState。冷流重复 collect 会重复请求,需 stateIn 共享。
追问 1 :Room 返回的 Flow 是冷还是热?
答 :形态是冷 Flow,但表变更时 Room 会重新 query 并 emit;通常 ViewModel 里 stateIn 转热再给 UI。
追问 2 :冷流能变热吗?
答 :stateIn 得 StateFlow,shareIn 得 SharedFlow,配合 SharingStarted 控制启停。
追问链 #2:stateIn 的 WhileSubscribed 解决什么问题? 🔥
标准回答 (≤200 字):冷流多订阅者会重复执行上游。stateIn 在 viewModelScope 内共享一份热 StateFlow。WhileSubscribed(5_000) 表示有订阅才启动,最后一个订阅消失后延迟 5 秒停上游,避免旋转屏瞬间停流又重启导致闪动与重复请求。Eagerly 无订阅也跑,费电。
追问 1 :stateIn 放 Repository 还是 ViewModel?
答 :常在 ViewModel 转 UI 态并管 SharingStarted;Repository 保持冷 Flow 更易单测复用。
追问 2 :initialValue 有什么用?
答 :首个真实值到达前 UI 有占位,例如 Loading 态,避免空白屏。
追问链 #3:StateFlow 和 LiveData 新代码怎么选? 🔥
标准回答 (≤200 字):新 Kotlin 工程默认 StateFlow :与 Flow 算子、combine、flatMapLatest 一致,Compose 一等公民。LiveData 仅遗留维护或 asFlow() 桥接。StateFlow 无内建生命周期感知,Fragment 用 repeatOnLifecycle(STARTED),Compose 用 collectAsStateWithLifecycle。一次性事件不用 StateFlow,防旋转重放。
追问 1 :为什么不直接暴露 MutableStateFlow?
答 :破坏封装,UI 可能直接改状态;应 asStateFlow() 只读,写操作走 ViewModel 方法。
追问 2 :StateFlow 和 SharedFlow 区别?
答 :StateFlow 必有初值、只关心最新态;SharedFlow 可无初值、配 replay,适合事件。
追问链 #4:repeatOnLifecycle 解决什么问题? ⭐
标准回答 (≤200 字):在 lifecycleScope.launch { flow.collect } 无边界收集时,Fragment STOPPED 仍收事件,浪费资源且可能改已销毁 View。repeatOnLifecycle(STARTED) 在低于 STARTED 时取消块内协程,回到前台重新 collect 并立刻收到 StateFlow 当前值。应使用 viewLifecycleOwner 而非 Fragment 自身 lifecycle。
追问 1 :collect 和 collectLatest 怎么选?
答 :搜索防抖用 collectLatest 取消旧处理;不可中断操作用 collect 顺序处理。
追问 2 :Compose 还要写 repeatOnLifecycle 吗?
答 :用 collectAsStateWithLifecycle() 封装,语义等价,更简洁。
追问链 #5:map 算子里的异常怎么处理? 💡
标准回答 (≤200 字):map 内抛异常会终止流,未捕获则 collect 失败。用 catch { emit(fallback) } 恢复,或在 map 前 runCatching 转 Result。不要在每个 collect 里 try-catch 业务异常------在 Flow 链上统一映射为 UiState.Error 更可测。
追问 1 :flowOn 和 withContext 区别?
答 :flowOn 切换其上游调度器,声明式链式;withContext 在 flow 块内切换,适合单点 IO。
追问 2 :distinctUntilChanged 何时必要?
答 :StateFlow 已内建相等跳过;冷流链上对 data class 或字段用,避免重复 UI 刷新。
完整链路一句通
Repository flow { } 或 Room 冷 Flow → map/filter/catch 清洗 → stateIn(WhileSubscribed) 转热 (或 ViewModel 手动 MutableStateFlow)→ asStateFlow() 只读暴露 → repeatOnLifecycle(STARTED) / collectAsStateWithLifecycle 收集 → UI 只读 state、事件上行 ViewModel。