kotlin-flow-state-event-modeling
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseKotlin Flow: state and event modeling
Kotlin Flow:状态与事件建模
Core principle
核心原则
Pick the primitive that matches replay, fan-out, and synchronous-read requirements. , , -backed flows, and cold differ in buffering, who sees each emission, and whether exists. Wrong choices drop events, leak sharing coroutines, or force fake domain sentinels into state.
StateFlowSharedFlowChannelFlow.value选择符合重放、扇出和同步读取需求的原语。 、、基于的流以及冷在缓冲机制、消息接收方以及是否支持属性方面存在差异。错误的选择会导致事件丢失、共享协程泄漏,或者迫使虚假领域哨兵值混入状态中。
StateFlowSharedFlowChannelFlow.valueWhen to use this skill
何时使用此技能
You're writing or reviewing Kotlin code involving:
- —
MutableStateFlow<T>(SomeSentinel),NoUser,Empty, etc. — because the real value is asyncLoading - called inside a function rather than assigned to a property
.stateIn(...) - on a flow whose
SharingStarted.WhileSubscribed(...)is read synchronously and must stay fresh.value - for navigation events, snackbars, or other one-shot emissions where loss would be a bug
MutableSharedFlow - on a
.map { }when consumers still need synchronousStateFlow.value - or update code that builds expensive objects inside
MutableStateFlow.value = _state.value.copy(...)update { ... }
你正在编写或评审包含以下内容的Kotlin代码:
- — 例如
MutableStateFlow<T>(SomeSentinel)、NoUser、Empty等 — 因为真实值是异步获取的Loading - 在函数内部调用而非赋值给属性
.stateIn(...) - 对需要同步读取且必须保持最新的流使用
.valueSharingStarted.WhileSubscribed(...) - 使用处理导航事件、snackbar或其他一旦丢失就会引发问题的一次性事件
MutableSharedFlow - 在上调用
StateFlow但消费者仍需同步读取.map { }.value - 使用或在
MutableStateFlow.value = _state.value.copy(...)内部构建开销较大的对象的更新代码update { ... }
SharedFlow for single-consumer fire-once events
单消费者一次性事件使用SharedFlow
SharedFlowChannelFlowkotlin
// ❌ BAD
private val _navEvents = MutableSharedFlow<NavigationEvent>()
val navEvents: SharedFlow<NavigationEvent> = _navEvents.asSharedFlow()
// ✅ GOOD
private val _navEvents = Channel<NavigationEvent>(Channel.BUFFERED)
val navEvents: Flow<NavigationEvent> = _navEvents.receiveAsFlow()Channel.receiveAsFlow()Channel.BUFFEREDtrySendSharedFlowSharedFlowChannelFlowkotlin
// ❌ 错误写法
private val _navEvents = MutableSharedFlow<NavigationEvent>()
val navEvents: SharedFlow<NavigationEvent> = _navEvents.asSharedFlow()
// ✅ 正确写法
private val _navEvents = Channel<NavigationEvent>(Channel.BUFFERED)
val navEvents: Flow<NavigationEvent> = _navEvents.receiveAsFlow()Channel.receiveAsFlow()Channel.BUFFEREDtrySendSharedFlowStateFlow polluted with invalid sentinel defaults
被无效哨兵默认值污染的StateFlow
StateFlowNoUserEmptyUserkotlin
// ❌ BAD — sentinel leaks into the type
class UserSession(private val db: Db) {
private val _user = MutableStateFlow<User>(NoUser)
val user: StateFlow<User> = _user.asStateFlow()
init { scope.launch { _user.value = db.load() } }
}One fix is phasing: don't expose the until the real value exists.
StateFlowkotlin
// ✅ GOOD — bootstrap suspends; observers only see real users
class UserSession(private val db: Db) {
private var _user: MutableStateFlow<User>? = null
val user: StateFlow<User>
get() = checkNotNull(_user) { "Call login() first" }
suspend fun login() {
_user = MutableStateFlow(db.load())
}
}If absence, loading, or error is a real state, model it explicitly (, , , etc.). The bug is a fake domain value masquerading as real data, not every initial value.
User?sealed interface UserUiStateResultStateFlowNoUserEmptyUserkotlin
// ❌ 错误写法 — 哨兵值侵入类型定义
class UserSession(private val db: Db) {
private val _user = MutableStateFlow<User>(NoUser)
val user: StateFlow<User> = _user.asStateFlow()
init { scope.launch { _user.value = db.load() } }
}一种修复方案是分阶段初始化:在真实值存在之前不暴露。
StateFlowkotlin
// ✅ 正确写法 — 启动时挂起;观察者仅能看到真实用户数据
class UserSession(private val db: Db) {
private var _user: MutableStateFlow<User>? = null
val user: StateFlow<User>
get() = checkNotNull(_user) { "请先调用login()" }
suspend fun login() {
_user = MutableStateFlow(db.load())
}
}如果缺失、加载中或错误状态是真实存在的状态,请显式建模(如、、等)。问题出在将虚假领域值伪装成真实数据,而非初始值本身。
User?sealed interface UserUiStateResultMutate MutableStateFlow with update { ... }
update { ... }使用update { ... }
修改MutableStateFlow
update { ... }Prefer over reading and writing it back. applies the transform atomically against the latest state, which avoids lost updates when multiple coroutines mutate the same state.
MutableStateFlow.update { current -> ... }.valueupdatekotlin
// BAD — read/modify/write can lose concurrent updates.
_state.value = _state.value.copy(
selectedId = id,
details = details,
)
// GOOD — transform starts from the latest state.
_state.update { current ->
current.copy(
selectedId = id,
details = details,
)
}Keep object creation outside the block unless it needs the current state. The update lambda can be retried, so expensive work or side effects inside it may run more than once:
updatekotlin
// GOOD — details does not depend on current state, so build it once.
val details = Details.from(response)
_state.update { current ->
current.copy(details = details)
}
// GOOD — derived value depends on current state, so compute it inside.
_state.update { current ->
val nextItems = current.items.replaceById(updatedItem)
current.copy(items = nextItems)
}The block should be a pure, fast state transformation: no network calls, database writes, logging side effects, random IDs, or time reads unless those values were captured before the block.
优先使用而非读取后再写回。会基于最新状态原子性地应用转换,避免多个协程修改同一状态时出现更新丢失的情况。
MutableStateFlow.update { current -> ... }.valueupdatekotlin
// 错误写法 — 读取/修改/写入可能丢失并发更新
_state.value = _state.value.copy(
selectedId = id,
details = details,
)
// 正确写法 — 转换基于最新状态
_state.update { current ->
current.copy(
selectedId = id,
details = details,
)
}除非需要当前状态,否则将对象创建放在块外部。更新lambda可能会重试,因此块内的开销较大的操作或副作用可能会执行多次:
updatekotlin
// 正确写法 — details不依赖当前状态,因此只需构建一次
val details = Details.from(response)
_state.update { current ->
current.copy(details = details)
}
// 正确写法 — 派生值依赖当前状态,因此在块内计算
_state.update { current ->
val nextItems = current.items.replaceById(updatedItem)
current.copy(items = nextItems)
}该块应是一个纯函数式的快速状态转换:除非值是在块外捕获的,否则不要包含网络请求、数据库写入、日志副作用、随机ID或时间读取操作。
stateIn()
inside a function
stateIn()在函数内部使用stateIn()
stateIn()kotlin
// ❌ BAD — new sharing coroutine every call
fun getPreferences(): StateFlow<Prefs> =
repo.prefsFlow.stateIn(scope, SharingStarted.Eagerly, Prefs.Default)Every call to launches a fresh coroutine on that never completes. Performance dies fast under repeated reads.
getPreferences()scopekotlin
// ✅ GOOD — one shared instance, computed once
val preferences: StateFlow<Prefs> =
repo.prefsFlow.stateIn(viewModelScope, SharingStarted.Eagerly, Prefs.Default)kotlin
// ❌ 错误写法 — 每次调用都会创建新的共享协程
fun getPreferences(): StateFlow<Prefs> =
repo.prefsFlow.stateIn(scope, SharingStarted.Eagerly, Prefs.Default)每次调用都会在上启动一个新的协程,且该协程永远不会结束。在频繁读取的情况下,性能会迅速下降。
getPreferences()scopekotlin
// ✅ 正确写法 — 单个共享实例,仅计算一次
val preferences: StateFlow<Prefs> =
repo.prefsFlow.stateIn(viewModelScope, SharingStarted.Eagerly, Prefs.Default)WhileSubscribed
with synchronous .value
WhileSubscribed.valueWhileSubscribed
与同步.value
WhileSubscribed.valueSharingStarted.WhileSubscribed(timeout).valueRule: if must be fresh or initialized without an active collector, use or explicit initialization. is fine when stale/cached values are acceptable and consumers primarily collect asynchronously.
.valueSharingStarted.EagerlyWhileSubscribedSharingStarted.WhileSubscribed(timeout).value规则: 如果必须保持最新或无需活跃收集器即可初始化,请使用或显式初始化。当过期/缓存值可接受且消费者主要以异步方式收集时,是合适的。
.valueSharingStarted.EagerlyWhileSubscribed.map
on StateFlow
loses .value
.mapStateFlow.value在StateFlow
上调用.map
会丢失.value
StateFlow.map.valuekotlin
// ❌ BAD — `name.value` won't compile; it's now a plain Flow
val name: Flow<String> = userState.map { it.name }If you need synchronous , terminate the chain with :
.value.stateIn(...)kotlin
// ✅ GOOD
val name: StateFlow<String> = userState
.map { it.name }
.stateIn(viewModelScope, SharingStarted.Eagerly, userState.value.name)Community “derived state flow” utilities run the transform on every read — only acceptable for fast, idempotent transforms. Default to .
.value.stateIn(...)kotlin
// ❌ 错误写法 — `name.value`无法编译;它现在是普通Flow
val name: Flow<String> = userState.map { it.name }如果需要同步读取,请在链式调用末尾添加:
.value.stateIn(...)kotlin
// ✅ 正确写法
val name: StateFlow<String> = userState
.map { it.name }
.stateIn(viewModelScope, SharingStarted.Eagerly, userState.value.name)社区提供的“派生状态流”工具会在每次读取时执行转换 — 仅适用于快速、幂等的转换。默认优先使用。
.value.stateIn(...)Decision: which Flow type?
决策:选择哪种Flow类型?
| Need | Primitive |
|---|---|
| State that always has a value, read by both async collectors and synchronous code | |
Hot stream, multiple subscribers, no requirement for synchronous | |
| Discrete events for one consumer, exactly-once handoff | Consider |
| Cold stream, one consumer per collection | Plain |
If you're tempted to reach for , ask: would dropping an emission be a bug, and how many consumers must see it? If one consumer must handle it exactly once, a may fit. If every observer must see it, model durable state or configure a broadcast stream deliberately.
SharedFlowChannel| 需求 | 原语 |
|---|---|
| 始终有值的状态,同时被异步收集器和同步代码读取 | |
热流,多个订阅者,无需同步读取 | |
| 面向单个消费者的离散事件,恰好一次传递 | 考虑使用 |
| 冷流,每次收集对应一个消费者 | 普通 |
如果你倾向于使用,请思考:丢失事件是否会引发问题?有多少消费者需要看到该事件?如果单个消费者必须恰好处理一次事件,可能更合适。如果所有观察者都必须看到该事件,请建模持久状态或刻意配置广播流。
SharedFlowChannelQuick reference
快速参考
| Symptom | Problem | Fix |
|---|---|---|
| Invalid placeholder default | Model absence explicitly or use phase initialization |
| Lossy default event stream | Consider |
| Per-call sharing coroutine | Make it a |
| Stale or initial data | |
| Lost | Terminate with |
| Non-atomic read/modify/write | |
Expensive object creation inside | Work can repeat if update retries | Build before |
| 症状 | 问题 | 修复方案 |
|---|---|---|
| 无效的占位符默认值 | 显式建模缺失状态或使用分阶段初始化 |
使用 | 默认事件流会丢失事件 | 考虑使用 |
| 每次调用都会创建共享协程 | 将其改为 |
| 数据过期或为初始值 | 使用 |
| 丢失 | 在链式调用末尾添加 |
| 非原子性的读取/修改/写入 | 使用 |
在 | 更新重试时会重复执行操作 | 在 |
Red flags during review
评审时的危险信号
| Thought | Reality |
|---|---|
"We need | Multiple subscribers change the semantics. |
"We'll use | Only if stale/initial |
| "I'll use a sentinel until real data loads" | Consumers treat it as real domain; prefer explicit UI/state modeling or phasing. |
"I'll construct the new object inside | The lambda may retry. Construct outside unless it depends on the current state. |
| 想法 | 实际情况 |
|---|---|
“我们需要 | 多个订阅者会改变语义。 |
“我们将使用 | 仅当过期/初始 |
| “我会使用哨兵值直到真实数据加载完成” | 消费者会将其视为真实领域值;优先选择显式UI/状态建模或分阶段初始化。 |
“我会在 | lambda可能会重试。除非依赖当前状态,否则请在外部构造。 |
Related
相关内容
- — scope ownership, init launches, fire-and-forget boundaries, cancellation,
kotlin-coroutines-structured-concurrencyrunBlocking - — collecting event flows and wiring side effects in Compose
compose-side-effects - — where state holders expose flows to UI
compose-state-holder-ui-split
- — 作用域所有权、初始化启动、即发即忘边界、取消、
kotlin-coroutines-structured-concurrencyrunBlocking - — 在Compose中收集事件流并连接副作用
compose-side-effects - — 状态持有者向UI暴露流的方式
compose-state-holder-ui-split