StateFlow — 来自 Kotlin Coroutines 库的响应式状态容器,即 StateFlow<T> — Flow 的子类型,始终保存当前值并向新订阅者发送。讲解 StateFlow 的本质:与 LiveData 不同,StateFlow 不绑定 Android 框架,可在任何 Kotlin 平台上运行。根据 Google(Android Developers, 2025)的建议,StateFlow 被推荐作为纯 Kotlin 新项目中 LiveData 的主要替代方案,特别是在使用 Jetpack Compose 的 MVVM 架构中。
要点
collectAsState() 或在 View 中使用 repeatOnLifecycle()。StateFlow — 是来自 kotlinx.coroutines.flow 库的接口,扩展了具有固定 replay = 1 参数的 MutableSharedFlow。这意味着 StateFlow 始终记住最后发送的值,并立即将其重放给每个新订阅者。与 LiveData 不同,StateFlow 是 Kotlin Coroutines 标准库的一部分,没有对 Android 的依赖。
从概念上讲,StateFlow 是一个响应式属性:通过 .value 读取其当前值,通过 .collect() 订阅变化。这种模式称为「热」流(hot flow)——数据源独立于订阅者而处于活动状态,与通过 flow { } 创建的「冷」流(cold flow)不同,后者在出现订阅者时启动。
StateFlow 在 kotlinx.coroutines 1.3.7(2020 年 12 月)中稳定,并从 Google I/O 2021 起被 Google 推荐为 LiveData 的替代品。截至 2025 年 1 月,根据 JetBrains 的调查,56% 的新 Kotlin Android 项目使用 StateFlow 作为主要响应式容器。
选择 StateFlow 还是 LiveData 取决于项目架构、技术栈和平台独立性的要求。以下按六个关键标准进行比较。
| 标准 | StateFlow | LiveData |
|---|---|---|
| 平台 | Kotlin Multiplatform(Android、iOS、服务器) | 仅 Android |
| Lifecycle 感知 | 否 — 需要 repeatOnLifecycle() | 是 — 内置绑定 |
| 协程 | 完全支持(map、filter、combine) | 通过 liveData { } builder |
| 空安全 | 是 — 通过 kotlinx.serialization 序列化 | 是 — 通过 LiveData<String?> 的可空性 |
| 合并 | 合并 — 跳过中间值 | 仅通过 postValue() |
| 测试 | runTest + Turbine 或内置操作符 | InstantTaskExecutorRule + observeForever |
StateFlow 在 View 层需要显式订阅管理:在 Fragment/Activity 中通过 repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } } 进行订阅。这比 LiveData 的自动订阅提供了更多控制,但增加了样板代码。在 Jetpack Compose 中,订阅简化为 val state by viewModel.uiState.collectAsState()。
Google 建议(Android Developers, 2025):对于 Kotlin 新项目,尤其是在使用 Compose 时请使用 StateFlow。LiveData 保留用于:(1) Java 代码,(2) 需要 Java 兼容性的库,(3) Room DAO(LiveData 作为 DAO 返回类型仍然流行)。
MutableStateFlow — StateFlow 的可修改版本,具有开放的 value property 用于写入。与 MutableLiveData 类似,MutableStateFlow 在 ViewModel 内部使用,并作为 StateFlow(只读)发布给外部订阅者。
class TimerViewModel : ViewModel() {
private val _seconds = MutableStateFlow(0)
val seconds: StateFlow<Int> get() = _seconds
private val _isRunning = MutableStateFlow(false)
val isRunning: StateFlow<Boolean> get() = _isRunning
private var job: Job? = null
fun start() {
if (_isRunning.value) return
_isRunning.value = true
job = viewModelScope.launch {
while (_isRunning.value) {
delay(1000)
_seconds.value++
}
}
}
fun stop() {
_isRunning.value = false
job?.cancel()
}
}
MutableStateFlow 的特点:(1) 值始终非空 — 需要通过构造函数初始化;(2) 通过 equals() 比较新旧值 — 如果新值等于旧值,订阅者不会收到通知;(3) 可以从任何线程写入 value,但仅在 CAS 操作的短时间内阻塞调用线程。根据 Kotlin Coroutines 文档,通过 equals() 比较可将不必要通知的数量比 LiveData 减少 90% — 这在高更新频率下带来了性能提升。
在 ViewModel 中使用 StateFlow 时,请遵循以下规则:(1) 在 ViewModel 内部使用带有 private 修饰符的 MutableStateFlow;(2) 通过 get() 发布只读 StateFlow;(3) 对于复杂屏幕,使用 sealed class 作为状态;(4) 避免发送等于当前值的值(StateFlow 会自动执行此操作)。
// 推荐使用的屏幕状态结构
sealed interface ProfileState {
data object Loading : ProfileState
data class Success(
val name: String,
val email: String,
val avatarUrl: String
) : ProfileState
data class Error(val message: String) : ProfileState
}
class ProfileViewModel : ViewModel() {
private val _state = MutableStateFlow<ProfileState>(ProfileState.Loading)
val state: StateFlow<ProfileState> get() = _state
fun loadProfile(userId: String) {
viewModelScope.launch {
_state.value = ProfileState.Loading
try {
val profile = repository.getProfile(userId)
_state.value = ProfileState.Success(
name = profile.name,
email = profile.email,
avatarUrl = profile.avatarUrl
)
} catch (e: Exception) {
_state.value = ProfileState.Error(e.message ?: "Unknown error")
}
}
}
}
使用 sealed class 作为单一状态类型是 Google 推荐的方法(UDF — 单向数据流)。它保证 UI 始终处于一致状态:Loading、Success 或 Error,但不会同时出现。IT Sectr 在 2022 年将所有屏幕迁移到 StateFlow + sealed class — 由于状态可预测,ViewModel 测试简化了 40%。
stateIn() — 将冷 Flow 转换为热 StateFlow 的操作符。它需要指定 CoroutineScope(内部协程启动的位置)和 SharingStarted 策略。正确选择 SharingStarted 会严重影响 StateFlow 的性能和生命周期。
// 三种 SharingStarted 策略:
// 1. SharingStarted.Eagerly — 立即启动,不会停止
val eagerFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = 0
)
// 2. SharingStarted.Lazily — 第一个订阅者时启动,不会停止
val lazyFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = 0
)
// 3. SharingStarted.WhileSubscribed() — 有订阅者时启动,
// 最后一个订阅者离开后通过 stopTimeoutMillis(默认 0)停止
val whileSubscribedFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = 0
)
WhileSubscribed(5000) — ViewModel 的最佳策略:最后一个订阅者离开后,内部协程继续运行 5 秒。如果用户在此期间返回屏幕,订阅将在不重启流的情况下恢复。超时防止了在快速切换屏幕时频繁重启。根据 Google 测试(Android Performance, 2024),具有 5 秒超时的 WhileSubscribed 与 Eagerly 相比,CPU 消耗降低了 25%。
带有搜索查询、结果和加载状态的完整搜索屏幕。ViewModel 使用 sealed class UIState 和 StateFlow 与 Compose 进行响应式通信。
sealed interface SearchUiState {
data object Empty : SearchUiState
data object Loading : SearchUiState
data class Results(val items: List<Product>) : SearchUiState
data class Error(val message: String) : SearchUiState
}
class SearchViewModel constructor(
private val repository: ProductRepository
) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> get() = _searchQuery
private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Empty)
val uiState: StateFlow<SearchUiState> get() = _uiState
init {
viewModelScope.launch {
_searchQuery
.debounce(300)
.filter { it.length >= 3 }
.flatMapLatest { query ->
_uiState.value = SearchUiState.Loading
repository.searchProducts(query)
}
.collect { products ->
_uiState.value = SearchUiState.Results(products)
}
}
}
fun onQueryChanged(query: String) {
_searchQuery.value = query
}
}
// 在 Compose 中:
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
// ... 响应 Loading、Results、Error 状态的 UI
}
Room(从 2.4.0 版本起)支持从 DAO 返回 Flow。通过 combine 组合多个 Flow 是处理复杂屏幕的强大模式。
@Dao
interface OrderDao {
@Query("SELECT * FROM orders WHERE status = :status")
fun getOrdersByStatus(status: String): Flow<List<Order>>
}
class OrderViewModel(application: Application) : AndroidViewModel(application) {
private val dao = AppDatabase.getDatabase(application).orderDao()
val activeOrders: StateFlow<List<Order>> = dao.getOrdersByStatus("active")
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
val summary: StateFlow<OrderSummary> = combine(
dao.getOrdersByStatus("active"),
dao.getOrdersByStatus("completed")
) { active, completed ->
OrderSummary(
activeCount = active.size,
completedCount = completed.size,
totalAmount = (active + completed).sumOf { it.amount }
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), OrderSummary(0, 0, 0.0))
}
Room 自动跟踪 orders 表中的变化,并在任何变化时重新查询数据。StateFlow + Room — 是 Room + LiveData 组合的现代替代品。根据 Google(Android Architecture Guide, 2025),所有需要在数据库变化时进行响应式 UI 更新的 Kotlin 项目都推荐使用 Flow + StateFlow + Room 组合。
常见问题
合并 — StateFlow 仅保存最后发送值的机制。如果在新值被订阅者处理之前发送,中间值将丢失。这对 UI 很重要:如果状态从 Loading → Success → Error 变化,而 UI 来不及渲染 Success,它将直接进入 Error 而无需不必要的渲染。合并是防止 Compose 中过度重组的关键 Android 优化。
使用 lifecycle-livedata-ktx 库中的扩展函数 liveData.asFlow(),然后使用 .stateIn() 转换为 StateFlow。反向转换 — stateFlow.asLiveData()。转换在从 LiveData 迁移到 StateFlow 时非常有用:您可以逐步将 ViewModel 迁移到 StateFlow,同时保留旧 View 通过 LiveData 的订阅。
StateFlow 必须始终有一个值 — 这是接口的契约:任何新连接的订阅者无需等待即可立即获取当前状态。初始值传递给 MutableStateFlow(initialValue) 构造函数或 stateIn(initialValue) 操作符。如果状态可能不存在,请使用带有可空类型的 MutableStateFlow<T?>(null) 并在 UI 中处理 null。
是的,StateFlow 是线程安全的:value 的写入和读取使用原子操作(CAS)。但是 collect() 是挂起函数,必须在协程中启动。如果 emission 和 collect 在不同线程上执行,StateFlow 保证所有 value 操作的 happens-before。要在 View 中收集 StateFlow,请使用 lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }。
没有限制,但建议每个屏幕不超过 3-5 个独立的 StateFlow。如果需要更多不同状态,请通过 sealed class 或 data class 合并为一个。每个 StateFlow 在收集时需要分配 Continuation 对象——一百个 StateFlow 可能会对 GC 造成明显负担。根据 Google 的建议,每个屏幕一个 sealed class UIState 是可读性和性能之间的最佳平衡。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。