Kotlin Flow 与 StateFlow:UI 单向数据流基础

文章目录

  • [第 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 单独扛不住:

  1. 持续变化:收藏列表、搜索建议、Room 表观察------数据会多次推送,不是「请求一次返回一次」。
  2. 多步变换 :原始 DTO 要经过 map、过滤、合并多个数据源,再交给 UI。
  3. 统一状态出口 :加载中、列表、错误应合成一个 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())
    }

ExploreViewModelMutableStateFlow 持有 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 观察)或在 flowwhile 轮询------本篇重点在机制,工程上优先 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 → DomainList → 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 时从下到上建立调用链。maptransform 的简化;异常在 map 内抛出会终止流,除非 catch 恢复。

踩坑

  • map 里做重量级 IO 却忘记上游 flowOn(IO)------阻塞默认调度器。
  • filter 后类型收窄,仍用旧类型访问字段------注意 Kotlin 智能转换在 lambda 内的限制。
  • 链过长却不 stateIn 共享------两个 Fragment collect 同一 Repository 冷流会双倍 IO。

动手练

  • C :为 favoritesFlow()map + catch,错误时 emit 空列表(≤6 行)。
  • AdistinctUntilChangeddistinctUntilChangedBy { it.id } 使用场景各一句。

第 4 章 冷转热:stateIn 与 shareIn

冷流每个收集者各跑一遍上游。多界面共享同一网络/数据库管道,或配置变更后重新 collect,应在合适层转为热流

API 产出 必备参数 典型用途
stateIn StateFlow initialValueSharingStarted UI 状态、需当前值
shareIn SharedFlow SharingStartedreplay 事件总线、无初值共享

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/shareInscope 内启动共享协程,上游只执行一份。WhileSubscribed 通过订阅计数控制启停,是 Android 省电关键。stateIninitialValue 在首个真实值到来前供 UI 显示占位(Loading)。

踩坑

  • stateInscopeGlobalScope → 泄漏与无法取消。
  • WhileSubscribed(0) 旋转屏立即停上游 → 频繁重启、列表闪动。
  • shareInStateFlow 用却不设 replay → 新订阅者收不到历史。

动手练

  • A :解释 WhileSubscribed(5_000)Eagerly 在电量上的差异。
  • B(起点) :两处 Fragment 各自 collect 同一 repository.observeFavorites() 冷流,如何用 stateIn 合并为一份(ViewModel 骨架 ≤10 行)?

第 5 章 StateFlow 与 MutableStateFlow:ViewModel 状态封装

新工程 UI 状态默认 StateFlow ,而非 LiveData 多字段或 Java 式回调。核心约定:私有可变、公开只读 ,更新用 updatevalue = 整体替换。

工作区 ExploreViewModel 是标准模板------ExploreUiStatedata classcopy 驱动不可变迁移:

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) }
        }
    }

链路解读

  1. _uiState 私有,外部只能读 uiState
  2. refresh()copy(isLoading = true),UI collect 到后显示进度。
  3. Repository 返回后再次 copy,列表与 loading 一次更新,避免多字段不同步。
  4. 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 时复用可变 Listadd → 相同引用 StateFlow 可能不发射。
  • initlaunch 却不处理 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 } 无边界收集。

踩坑

  • onCreatelifecycleScope 而非 viewLifecycleOwner 收集 → Fragment View 销毁后仍更新 binding。
  • collect 里启动新 launch 不随生命周期取消 → 泄漏。
  • 把导航副作用放在 collect 里不设 Consumed 标记 → 旋转重复导航(应走 SharedFlow 事件通道)。

动手练

  • C :为 ExploreFragmentobserveViewModelcollectLatest 实验,说明与当前 collect 的差异(注释即可)。
  • D 验收清单:旋转屏 + 切后台再回前台,确认列表与 loading 不错乱、无重复请求异常日志。

面试速查 · 追问链

追问链 #1:Flow 为什么是冷流?和 StateFlow 怎么区分? 🔥

标准回答 (≤200 字):Flow 只有下游 collect 时上游 flow {} 才执行,每个收集者独立跑一份,类似挂起版 SequenceStateFlow 是热流,始终有当前值,多收集者共享。Repository 暴露冷 Flow 拉数据;ViewModel 用 StateFlowUiState。冷流重复 collect 会重复请求,需 stateIn 共享。

追问 1 :Room 返回的 Flow 是冷还是热?

:形态是冷 Flow,但表变更时 Room 会重新 query 并 emit;通常 ViewModel 里 stateIn 转热再给 UI。

追问 2 :冷流能变热吗?

stateInStateFlowshareInSharedFlow,配合 SharingStarted 控制启停。


追问链 #2:stateIn 的 WhileSubscribed 解决什么问题? 🔥

标准回答 (≤200 字):冷流多订阅者会重复执行上游。stateInviewModelScope 内共享一份热 StateFlowWhileSubscribed(5_000) 表示有订阅才启动,最后一个订阅消失后延迟 5 秒停上游,避免旋转屏瞬间停流又重启导致闪动与重复请求。Eagerly 无订阅也跑,费电。

追问 1stateIn 放 Repository 还是 ViewModel?

:常在 ViewModel 转 UI 态并管 SharingStarted;Repository 保持冷 Flow 更易单测复用。

追问 2initialValue 有什么用?

:首个真实值到达前 UI 有占位,例如 Loading 态,避免空白屏。


追问链 #3:StateFlow 和 LiveData 新代码怎么选? 🔥

标准回答 (≤200 字):新 Kotlin 工程默认 StateFlow :与 Flow 算子、combineflatMapLatest 一致,Compose 一等公民。LiveData 仅遗留维护或 asFlow() 桥接。StateFlow 无内建生命周期感知,Fragment 用 repeatOnLifecycle(STARTED),Compose 用 collectAsStateWithLifecycle。一次性事件不用 StateFlow,防旋转重放。

追问 1 :为什么不直接暴露 MutableStateFlow

:破坏封装,UI 可能直接改状态;应 asStateFlow() 只读,写操作走 ViewModel 方法。

追问 2StateFlowSharedFlow 区别?

:StateFlow 必有初值、只关心最新态;SharedFlow 可无初值、配 replay,适合事件。


追问链 #4:repeatOnLifecycle 解决什么问题? ⭐

标准回答 (≤200 字):在 lifecycleScope.launch { flow.collect } 无边界收集时,Fragment STOPPED 仍收事件,浪费资源且可能改已销毁 View。repeatOnLifecycle(STARTED) 在低于 STARTED 时取消块内协程,回到前台重新 collect 并立刻收到 StateFlow 当前值。应使用 viewLifecycleOwner 而非 Fragment 自身 lifecycle。

追问 1collectcollectLatest 怎么选?

:搜索防抖用 collectLatest 取消旧处理;不可中断操作用 collect 顺序处理。

追问 2 :Compose 还要写 repeatOnLifecycle 吗?

:用 collectAsStateWithLifecycle() 封装,语义等价,更简洁。


追问链 #5:map 算子里的异常怎么处理? 💡

标准回答 (≤200 字):map 内抛异常会终止流,未捕获则 collect 失败。用 catch { emit(fallback) } 恢复,或在 maprunCatchingResult。不要在每个 collect 里 try-catch 业务异常------在 Flow 链上统一映射为 UiState.Error 更可测。

追问 1flowOnwithContext 区别?

flowOn 切换其上游调度器,声明式链式;withContextflow 块内切换,适合单点 IO。

追问 2distinctUntilChanged 何时必要?

StateFlow 已内建相等跳过;冷流链上对 data class 或字段用,避免重复 UI 刷新。


完整链路一句通

Repository flow { } 或 Room 冷 Flowmap/filter/catch 清洗stateIn(WhileSubscribed) 转热 (或 ViewModel 手动 MutableStateFlow)→ asStateFlow() 只读暴露repeatOnLifecycle(STARTED) / collectAsStateWithLifecycle 收集UI 只读 state、事件上行 ViewModel


相关推荐

Kotlin 语法与空安全:Android 开发第一课

Kotlin 作用域函数:let/apply 工程选型

相关推荐
艾伦野鸽ggg18 小时前
JavaScript 浏览器本地存储
开发语言·javascript·ecmascript
AOwhisky19 小时前
Python 学习笔记(第十一期)——运维自动化(上·后篇):进程级监控与子进程管理——psutil进阶
运维·开发语言·python·学习·云原生·运维开发
人工干智能19 小时前
Python 的链式调用:一连串的“点”调用
开发语言·python
就不掉头发20 小时前
如何优化程序的性能
开发语言
手写码匠20 小时前
MCP协议从零实现:手写一个完整的 Model Context Protocol 服务器与客户端
开发语言·数据结构
良木生香20 小时前
【C++初阶】STL—— Stack & Queue 从入门到精通:容器适配器、迭代器与经典面试题
java·开发语言·c++·算法·zookeeper
咩咩啃树皮20 小时前
第36篇:JavaScript 原型与原型链终极吃透——JS万物皆对象、继承核心原理
开发语言·javascript·ecmascript
lbb 小魔仙20 小时前
Python 性能分析工具实战:cProfile、memory_profiler、line_profiler 使用指南
开发语言·python
Kapaseker20 小时前
Sequence 一定比 List 快?等等,我们先从基础讲起
android·kotlin