StateFlow — StateFlow 与 LiveData 在 Android 中的比较

作者: IT Sectr 发布日期: 2026-02-20 阅读时间: 9 分钟

StateFlow — 来自 Kotlin Coroutines 库的响应式状态容器,即 StateFlow<T> — Flow 的子类型,始终保存当前值并向新订阅者发送。讲解 StateFlow 的本质:与 LiveData 不同,StateFlow 不绑定 Android 框架,可在任何 Kotlin 平台上运行。根据 Google(Android Developers, 2025)的建议,StateFlow 被推荐作为纯 Kotlin 新项目中 LiveData 的主要替代方案,特别是在使用 Jetpack Compose 的 MVVM 架构中。

要点

  • StateFlow — 来自 kotlinx.coroutines.flow 的状态持有者,始终保存一个当前值并在订阅时发送。
  • MutableStateFlow — 具有可变 value property 的可修改 StateFlow,在 ViewModel 内部使用并作为 StateFlow 发布。
  • collect() — 用于订阅变化的 Flow 终端操作符;UI 在 Compose 中使用 collectAsState() 或在 View 中使用 repeatOnLifecycle()
  • StateFlow vs LiveData:StateFlow 不依赖 Lifecycle,需要显式管理订阅,但支持协程和多平台。
  • stateIn() — 将任何 Flow 转换为 StateFlow 的操作符,可配置 SharingStarted 策略。

什么是 Kotlin 中的 StateFlow?

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 vs LiveData:主要区别

选择 StateFlow 还是 LiveData 取决于项目架构、技术栈和平台独立性的要求。以下按六个关键标准进行比较。

标准StateFlowLiveData
平台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:发布与订阅

MutableStateFlow — StateFlow 的可修改版本,具有开放的 value property 用于写入。与 MutableLiveData 类似,MutableStateFlow 在 ViewModel 内部使用,并作为 StateFlow(只读)发布给外部订阅者。

kotlin
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:最佳实践

在 ViewModel 中使用 StateFlow 时,请遵循以下规则:(1) 在 ViewModel 内部使用带有 private 修饰符的 MutableStateFlow;(2) 通过 get() 发布只读 StateFlow;(3) 对于复杂屏幕,使用 sealed class 作为状态;(4) 避免发送等于当前值的值(StateFlow 会自动执行此操作)。

kotlin
// 推荐使用的屏幕状态结构
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() 和 SharingStarted:三种策略

stateIn() — 将冷 Flow 转换为热 StateFlow 的操作符。它需要指定 CoroutineScope(内部协程启动的位置)和 SharingStarted 策略。正确选择 SharingStarted 会严重影响 StateFlow 的性能和生命周期。

kotlin
// 三种 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%

代码示例:Kotlin 中的 StateFlow

示例 1:使用 StateFlow 和 Compose 的 ViewModel

带有搜索查询、结果和加载状态的完整搜索屏幕。ViewModel 使用 sealed class UIState 和 StateFlow 与 Compose 进行响应式通信。

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

示例 2:StateFlow 与 Room 和 combine

Room(从 2.4.0 版本起)支持从 DAO 返回 Flow。通过 combine 组合多个 Flow 是处理复杂屏幕的强大模式。

kotlin
@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 中的合并(conflation)是什么?

合并 — StateFlow 仅保存最后发送值的机制。如果在新值被订阅者处理之前发送,中间值将丢失。这对 UI 很重要:如果状态从 Loading → Success → Error 变化,而 UI 来不及渲染 Success,它将直接进入 Error 而无需不必要的渲染。合并是防止 Compose 中过度重组的关键 Android 优化。

如何将 LiveData 转换为 StateFlow?

使用 lifecycle-livedata-ktx 库中的扩展函数 liveData.asFlow(),然后使用 .stateIn() 转换为 StateFlow。反向转换 — stateFlow.asLiveData()。转换在从 LiveData 迁移到 StateFlow 时非常有用:您可以逐步将 ViewModel 迁移到 StateFlow,同时保留旧 View 通过 LiveData 的订阅。

为什么 StateFlow 需要初始值?

StateFlow 必须始终有一个值 — 这是接口的契约:任何新连接的订阅者无需等待即可立即获取当前状态。初始值传递给 MutableStateFlow(initialValue) 构造函数或 stateIn(initialValue) 操作符。如果状态可能不存在,请使用带有可空类型的 MutableStateFlow<T?>(null) 并在 UI 中处理 null。

StateFlow 是线程安全的吗?

是的,StateFlow 是线程安全的:value 的写入和读取使用原子操作(CAS)。但是 collect() 是挂起函数,必须在协程中启动。如果 emission 和 collect 在不同线程上执行,StateFlow 保证所有 value 操作的 happens-before。要在 View 中收集 StateFlow,请使用 lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }。

一个 ViewModel 中能存储多少个 StateFlow?

没有限制,但建议每个屏幕不超过 3-5 个独立的 StateFlow。如果需要更多不同状态,请通过 sealed class 或 data class 合并为一个。每个 StateFlow 在收集时需要分配 Continuation 对象——一百个 StateFlow 可能会对 GC 造成明显负担。根据 Google 的建议,每个屏幕一个 sealed class UIState 是可读性和性能之间的最佳平衡。

总结

  • StateFlow — 来自 Kotlin Coroutines(replay=1)的热响应式容器,始终保存最新的值。
  • StateFlow vs LiveData:StateFlow 不依赖 Lifecycle,支持协程和多平台;LiveData — 自动订阅。
  • 具有 private set 的 MutableStateFlow 和发布只读 StateFlow — ViewModel 的标准模式。
  • Sealed class 作为 UIState — Google 推荐的 UDF 方法,用于管理复杂的屏幕状态。
  • stateIn() 配合 WhileSubscribed(5000) — 将冷 Flow 转换为 ViewModel 中 StateFlow 的最佳策略。
  • Room 从 DAO 返回 Flow — StateFlow + combine + Room 取代 Room + LiveData 组合。
  • Google 推荐在新 Kotlin 项目中使用 StateFlow,尤其是与 Jetpack Compose 结合使用时。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读