kotlin-flow-state-event-modeling

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Kotlin Flow: state and event modeling

Kotlin Flow:状态与事件建模

Core principle

核心原则

Pick the primitive that matches replay, fan-out, and synchronous-read requirements.
StateFlow
,
SharedFlow
,
Channel
-backed flows, and cold
Flow
differ in buffering, who sees each emission, and whether
.value
exists. Wrong choices drop events, leak sharing coroutines, or force fake domain sentinels into state.
选择符合重放、扇出和同步读取需求的原语。
StateFlow
SharedFlow
、基于
Channel
的流以及冷
Flow
在缓冲机制、消息接收方以及是否支持
.value
属性方面存在差异。错误的选择会导致事件丢失、共享协程泄漏,或者迫使虚假领域哨兵值混入状态中。

When to use this skill

何时使用此技能

You're writing or reviewing Kotlin code involving:
  • MutableStateFlow<T>(SomeSentinel)
    NoUser
    ,
    Empty
    ,
    Loading
    , etc. — because the real value is async
  • .stateIn(...)
    called inside a function rather than assigned to a property
  • SharingStarted.WhileSubscribed(...)
    on a flow whose
    .value
    is read synchronously and must stay fresh
  • MutableSharedFlow
    for navigation events, snackbars, or other one-shot emissions where loss would be a bug
  • .map { }
    on a
    StateFlow
    when consumers still need synchronous
    .value
  • MutableStateFlow.value = _state.value.copy(...)
    or update code that builds expensive objects inside
    update { ... }
你正在编写或评审包含以下内容的Kotlin代码:
  • MutableStateFlow<T>(SomeSentinel)
    — 例如
    NoUser
    Empty
    Loading
    等 — 因为真实值是异步获取的
  • 在函数内部调用
    .stateIn(...)
    而非赋值给属性
  • 对需要同步读取
    .value
    且必须保持最新的流使用
    SharingStarted.WhileSubscribed(...)
  • 使用
    MutableSharedFlow
    处理导航事件、snackbar或其他一旦丢失就会引发问题的一次性事件
  • StateFlow
    上调用
    .map { }
    但消费者仍需同步读取
    .value
  • 使用
    MutableStateFlow.value = _state.value.copy(...)
    或在
    update { ... }
    内部构建开销较大的对象的更新代码

SharedFlow for single-consumer fire-once events

单消费者一次性事件使用SharedFlow

SharedFlow
defaults have no replay buffer. If nothing is collecting at the exact instant of emission, the event is gone. For a single UI consumer handling exactly-once events such as navigation or snackbars, a buffered
Channel
exposed as a
Flow
often matches the semantics better:
kotlin
// ❌ 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()
is fan-out, not broadcast: with multiple collectors, each event is delivered to one collector.
Channel.BUFFERED
is bounded, so sends can suspend and
trySend
can fail. If multiple observers must all see the same event, use explicit state, durable storage, or a deliberately configured
SharedFlow
instead.
SharedFlow
默认没有重放缓冲区。如果在发送事件的瞬间没有任何收集器,该事件就会丢失。对于处理导航或snackbar等恰好一次事件的单个UI消费者,使用带缓冲的
Channel
并暴露为
Flow
通常更符合语义:
kotlin
// ❌ 错误写法
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.BUFFERED
是有界的,因此发送操作可能会挂起,
trySend
也可能失败。如果多个观察者必须都看到同一个事件,请改用显式状态、持久存储或刻意配置的
SharedFlow

StateFlow polluted with invalid sentinel defaults

被无效哨兵默认值污染的StateFlow

StateFlow
forces an initial value. When the real value is async, developers sometimes invent fake domain values —
NoUser
,
EmptyUser
, placeholder IDs — and every consumer is forced to treat that sentinel as real data.
kotlin
// ❌ 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
StateFlow
until the real value exists.
kotlin
// ✅ 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 (
User?
,
sealed interface UserUiState
,
Result
, etc.). The bug is a fake domain value masquerading as real data, not every initial value.
StateFlow
必须设置初始值。当真实值是异步获取时,开发者有时会虚构虚假领域值 — 如
NoUser
EmptyUser
、占位符ID — 而所有消费者都被迫将该哨兵值视为真实数据。
kotlin
// ❌ 错误写法 — 哨兵值侵入类型定义
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() } }
}
一种修复方案是分阶段初始化:在真实值存在之前不暴露
StateFlow
kotlin
// ✅ 正确写法 — 启动时挂起;观察者仅能看到真实用户数据
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 UserUiState
Result
等)。问题出在将虚假领域值伪装成真实数据,而非初始值本身。

Mutate MutableStateFlow with
update { ... }

使用
update { ... }
修改MutableStateFlow

Prefer
MutableStateFlow.update { current -> ... }
over reading
.value
and writing it back.
update
applies the transform atomically against the latest state, which avoids lost updates when multiple coroutines mutate the same state.
kotlin
// 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
update
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:
kotlin
// 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 -> ... }
而非读取
.value
后再写回。
update
会基于最新状态原子性地应用转换,避免多个协程修改同一状态时出现更新丢失的情况。
kotlin
// 错误写法 — 读取/修改/写入可能丢失并发更新
_state.value = _state.value.copy(
    selectedId = id,
    details = details,
)

// 正确写法 — 转换基于最新状态
_state.update { current ->
    current.copy(
        selectedId = id,
        details = details,
    )
}
除非需要当前状态,否则将对象创建放在
update
块外部。更新lambda可能会重试,因此块内的开销较大的操作或副作用可能会执行多次:
kotlin
// 正确写法 — 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()

kotlin
// ❌ BAD — new sharing coroutine every call
fun getPreferences(): StateFlow<Prefs> =
    repo.prefsFlow.stateIn(scope, SharingStarted.Eagerly, Prefs.Default)
Every call to
getPreferences()
launches a fresh coroutine on
scope
that never completes. Performance dies fast under repeated reads.
kotlin
// ✅ 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()
都会在
scope
上启动一个新的协程,且该协程永远不会结束。在频繁读取的情况下,性能会迅速下降。
kotlin
// ✅ 正确写法 — 单个共享实例,仅计算一次
val preferences: StateFlow<Prefs> =
    repo.prefsFlow.stateIn(viewModelScope, SharingStarted.Eagerly, Prefs.Default)

WhileSubscribed
with synchronous
.value

WhileSubscribed
与同步
.value

SharingStarted.WhileSubscribed(timeout)
disconnects the upstream when there are no active collectors. While disconnected,
.value
returns the last cached value, which may be stale or still the initial value.
Rule: if
.value
must be fresh or initialized without an active collector, use
SharingStarted.Eagerly
or explicit initialization.
WhileSubscribed
is fine when stale/cached values are acceptable and consumers primarily collect asynchronously.
SharingStarted.WhileSubscribed(timeout)
会在没有活跃收集器时断开上游连接。断开连接期间,
.value
会返回最后缓存的值,该值可能已过期或仍是初始值。
规则: 如果
.value
必须保持最新或无需活跃收集器即可初始化,请使用
SharingStarted.Eagerly
或显式初始化。当过期/缓存值可接受且消费者主要以异步方式收集时,
WhileSubscribed
是合适的。

.map
on
StateFlow
loses
.value

StateFlow
上调用
.map
会丢失
.value

kotlin
// ❌ BAD — `name.value` won't compile; it's now a plain Flow
val name: Flow<String> = userState.map { it.name }
If you need synchronous
.value
, terminate the chain with
.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
.value
read — only acceptable for fast, idempotent transforms. Default to
.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类型?

NeedPrimitive
State that always has a value, read by both async collectors and synchronous code
StateFlow
, often with
SharingStarted.Eagerly
when
.value
matters
Hot stream, multiple subscribers, no requirement for synchronous
.value
SharedFlow
Discrete events for one consumer, exactly-once handoffConsider
Channel(BUFFERED).receiveAsFlow()
Cold stream, one consumer per collectionPlain
Flow
If you're tempted to reach for
SharedFlow
, ask: would dropping an emission be a bug, and how many consumers must see it? If one consumer must handle it exactly once, a
Channel
may fit. If every observer must see it, model durable state or configure a broadcast stream deliberately.
需求原语
始终有值的状态,同时被异步收集器同步代码读取
StateFlow
,当
.value
很重要时通常搭配
SharingStarted.Eagerly
热流,多个订阅者,无需同步读取
.value
SharedFlow
面向单个消费者的离散事件,恰好一次传递考虑使用
Channel(BUFFERED).receiveAsFlow()
冷流,每次收集对应一个消费者普通
Flow
如果你倾向于使用
SharedFlow
,请思考:丢失事件是否会引发问题?有多少消费者需要看到该事件?如果单个消费者必须恰好处理一次事件,
Channel
可能更合适。如果所有观察者都必须看到该事件,请建模持久状态或刻意配置广播流。

Quick reference

快速参考

SymptomProblemFix
MutableStateFlow<X>(FakeDomainValue)
Invalid placeholder defaultModel absence explicitly or use phase initialization
MutableSharedFlow<Event>
for single-consumer nav/snackbar
Lossy default event streamConsider
Channel(BUFFERED).receiveAsFlow()
fun foo() = flow.stateIn(...)
Per-call sharing coroutineMake it a
val
/ shared instance
WhileSubscribed
+
.value
must be fresh/initialized
Stale or initial data
SharingStarted.Eagerly
or explicit initialization
stateFlow.map { ... }
consumed as state
Lost
.value
Terminate with
.stateIn(...)
_state.value = _state.value.copy(...)
Non-atomic read/modify/write
_state.update { it.copy(...) }
Expensive object creation inside
update { ... }
that doesn't use current state
Work can repeat if update retriesBuild before
update
; keep only current-state transforms inside
症状问题修复方案
MutableStateFlow<X>(FakeDomainValue)
无效的占位符默认值显式建模缺失状态或使用分阶段初始化
使用
MutableSharedFlow<Event>
处理单消费者导航/snackbar事件
默认事件流会丢失事件考虑使用
Channel(BUFFERED).receiveAsFlow()
fun foo() = flow.stateIn(...)
每次调用都会创建共享协程将其改为
val
/ 共享实例
WhileSubscribed
+
.value
必须保持最新/已初始化
数据过期或为初始值使用
SharingStarted.Eagerly
或显式初始化
stateFlow.map { ... }
作为状态使用
丢失
.value
属性
在链式调用末尾添加
.stateIn(...)
_state.value = _state.value.copy(...)
非原子性的读取/修改/写入使用
_state.update { it.copy(...) }
update { ... }
内部创建不依赖当前状态的开销较大的对象
更新重试时会重复执行操作
update
外部构建对象;仅将依赖当前状态的转换放在块内

Red flags during review

评审时的危险信号

ThoughtReality
"We need
SharedFlow
because there are multiple subscribers"
Multiple subscribers change the semantics.
Channel.receiveAsFlow()
is not broadcast; choose the event model deliberately.
"We'll use
WhileSubscribed
to save resources"
Only if stale/initial
.value
reads are acceptable. Verify before applying.
"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
update
because it's convenient"
The lambda may retry. Construct outside unless it depends on the current state.
想法实际情况
“我们需要
SharedFlow
因为有多个订阅者”
多个订阅者会改变语义。
Channel.receiveAsFlow()
不是广播;请刻意选择事件模型。
“我们将使用
WhileSubscribed
来节省资源”
仅当过期/初始
.value
读取可接受时才适用。应用前请验证。
“我会使用哨兵值直到真实数据加载完成”消费者会将其视为真实领域值;优先选择显式UI/状态建模或分阶段初始化。
“我会在
update
内部构造新对象,因为这样很方便”
lambda可能会重试。除非依赖当前状态,否则请在外部构造。

Related

相关内容

  • kotlin-coroutines-structured-concurrency
    — scope ownership, init launches, fire-and-forget boundaries, cancellation,
    runBlocking
  • compose-side-effects
    — collecting event flows and wiring side effects in Compose
  • compose-state-holder-ui-split
    — where state holders expose flows to UI
  • kotlin-coroutines-structured-concurrency
    — 作用域所有权、初始化启动、即发即忘边界、取消、
    runBlocking
  • compose-side-effects
    — 在Compose中收集事件流并连接副作用
  • compose-state-holder-ui-split
    — 状态持有者向UI暴露流的方式