Kotlin 协程:StateFlow 与 SharedFlow 深度解析

在现代 Android 开发中,响应式编程已经成为处理 UI 状态和数据流的标准方式。Kotlin 协程库提供的 StateFlowSharedFlowLiveData 的强大替代品,它们更加灵活、功能更丰富,并且与协程生态无缝集成。本文将深入探讨这两种 Flow 的特点、使用场景以及最佳实践。


一、Flow 家族概览

在深入之前,我们先理清 Kotlin 中 Flow 的层次结构:

  • Flow<T>:冷流(Cold Stream),每次收集都会从头开始发射数据
  • StateFlow<T>:热流(Hot Stream),始终持有一个状态值,新订阅者立即获得当前值
  • SharedFlow<T>:热流(Hot Stream),可配置的回放和缓冲策略,更通用的热流实现

冷流 vs 热流:冷流是"按需生产",只有消费者订阅时才开始发射数据;热流是"持续生产",无论是否有消费者,数据都会持续产生。


二、StateFlow:状态管理的利器

2.1 什么是 StateFlow?

StateFlow 是一个带有状态的可观察数据流,它始终持有一个最新的状态值,也不能为空。任何新的收集器(Collector)都会立即收到当前的最新值。

kotlin 复制代码
// 创建 StateFlow
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

// 更新状态
_uiState.value = UiState.Success(data)

2.2 核心特性

特性 说明
必须有初始值 创建时必须提供默认值,不会出现 null 状态的歧义
仅保留最新值 只缓存一个值,新值会覆盖旧值
去重机制 相同值(也就是状态不变)不会触发重复通知
支持状态读取 可以随时通过 .value 同步读取当前状态

2.3 与 LiveData 的对比

kotlin 复制代码
// LiveData 写法
class MyViewModel : ViewModel() {
    private val _count = MutableLiveData(0)
    val count: LiveData<Int> = _count
    
    fun increment() {
        _count.value = _count.value!! + 1
    }
}

// StateFlow 等价写法
class MyViewModel : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()
    
    fun increment() {
        _count.update { it + 1 }  // 线程安全的原子更新,内部使用了CAS+失败重试操作
    }
}

StateFlow 的优势:

  • ✅ 天然支持协程,无需 viewModelScope 转换
  • ✅ 支持丰富的 Flow 操作符(mapfiltercombine 等)
  • ✅ 默认支持防抖(值不变不通知)
  • ✅ 无生命周期依赖,可在任意层使用

2.4 在 UI 层收集 StateFlow

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->
                    updateUi(state)
                }
            }
        }
    }
}

2.5 状态更新的最佳实践

kotlin 复制代码
// ❌ 错误:直接修改状态对象(如果 UiState 是 data class)
_uiState.value.name = "New Name"

// ✅ 正确:创建新的状态对象
data class UiState(val name: String, val age: Int)

_uiState.value = _uiState.value.copy(name = "New Name")

// ✅ 更优:使用 update 方法(线程安全)
_uiState.update { currentState ->
    currentState.copy(name = "New Name")
}

三、SharedFlow:通用事件总线

3.1 什么是 SharedFlow?

SharedFlow 是一种不带状态的数据流,可以没有初始值,支持配置回放(replay)和缓冲(buffer)策略,非常适合处理一次性事件(如 Toast、导航、Snackbar)。

kotlin 复制代码
// 创建 SharedFlow
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()

// 发送事件
_events.emit(UiEvent.ShowToast("操作成功"))

3.2 核心配置参数

kotlin 复制代码
val sharedFlow = MutableSharedFlow<Event>(
    replay = 1,           // 新订阅者收到的历史事件数量
    extraBufferCapacity = 2,  // 超出 replay 的额外缓冲,给现有订阅者使用
    onBufferOverflow = BufferOverflow.DROP_OLDEST  // 缓冲溢出策略
)
参数 说明 默认值
replay 新订阅者能收到的最近 N 个值 0
extraBufferCapacity 超出 replay 的额外缓冲 0
onBufferOverflow 缓冲区满时的处理策略 BufferOverflow.SUSPEND

溢出策略:

  • SUSPEND:发送方挂起等待(默认,需确保有消费者)
  • DROP_OLDEST:丢弃最旧的数据
  • DROP_LATEST:丢弃最新的数据

注:buffer 和溢出策略只在有收集器时生效。没有收集器时,emit 不会挂起:有 replay 时更新 replayCache,无 replay 时直接丢弃。

3.3 SharedFlow 的典型应用场景

场景一:一次性事件(Event)
kotlin 复制代码
sealed class UiEvent {
    data class NavigateTo(val route: String) : UiEvent()
    data class ShowSnackbar(val message: String) : UiEvent()
    data class ShowToast(val message: String) : UiEvent()
}

class NewsViewModel : ViewModel() {
    private val _uiEvent = MutableSharedFlow<UiEvent>()
    val uiEvent = _uiEvent.asSharedFlow()
    
    fun onNewsClick(newsId: String) {
        viewModelScope.launch {
            _uiEvent.emit(UiEvent.NavigateTo("detail/$newsId"))
        }
    }
}
场景二:带缓冲的事件流
kotlin 复制代码
class SensorViewModel : ViewModel() {
    // 保留最近 5 个传感器读数
    private val _sensorData = MutableSharedFlow<SensorReading>(
        replay = 5,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    val sensorData = _sensorData.asSharedFlow()
}

3.4 对比:为什么不用 StateFlow 处理事件?

kotlin 复制代码
// ❌ 错误:用 StateFlow 处理 Toast 事件
private val _toastEvent = MutableStateFlow<String?>(null)

// 问题1:需要手动清空状态
// 问题2:屏幕旋转后事件会重新触发
// 问题3:去重机制可能导致连续相同事件丢失

// ✅ 正确:用 SharedFlow 处理一次性事件
private val _toastEvent = MutableSharedFlow<String>()

四、StateFlow vs SharedFlow:如何选择?

维度 StateFlow SharedFlow
初始值 必须有 可选(默认为空)
当前状态读取 支持 .value 不支持直接读取
去重 自动去重(equals) 不去重,每个值都发射
适用场景 UI 状态(State) 一次性事件(Event)
生命周期 长期持有 瞬时消费
典型例子 加载状态、列表数据、用户信息 Toast、导航、Snackbar、日志

4.1 决策流程图

复制代码
需要表示状态吗?
├── 是 → 需要读取当前值?
│         ├── 是 → StateFlow
│         └── 否 → SharedFlow (replay=1)
└── 否 → 一次性事件?
          ├── 是 → SharedFlow (replay=0)
          └── 否 → 普通 Flow

五、高级技巧与最佳实践

5.1 结合使用 StateFlow + SharedFlow

在 MVVM 架构中,推荐同时使用两者:

kotlin 复制代码
class ProfileViewModel : ViewModel() {
    // StateFlow:UI 状态
    private val _uiState = MutableStateFlow(ProfileUiState())
    val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
    
    // SharedFlow:一次性事件
    private val _events = MutableSharedFlow<ProfileEvent>()
    val events: SharedFlow<ProfileEvent> = _events.asSharedFlow()
    
    fun loadProfile(userId: String) {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            
            try {
                val profile = repository.getProfile(userId)
                _uiState.update { it.copy(isLoading = false, profile = profile) }
            } catch (e: Exception) {
                _uiState.update { it.copy(isLoading = false) }
                _events.emit(ProfileEvent.ShowError(e.message ?: "未知错误"))
            }
        }
    }
}

5.2 使用 combine 合并多个 StateFlow

kotlin 复制代码
class SearchViewModel : ViewModel() {
    private val _query = MutableStateFlow("")
    private val _sortOrder = MutableStateFlow(SortOrder.RELEVANCE)
    
    // 自动响应任一状态变化
    val searchResults: StateFlow<List<Item>> = combine(
        _query,
        _sortOrder,
        ::Pair
    ).flatMapLatest { (query, sort) ->
        repository.search(query, sort)
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = emptyList()
    )
}

5.3 stateIn 操作符:冷流转热流

kotlin 复制代码
// 将普通 Flow 转换为 StateFlow
val stateFlow = repository.getDataFlow()
    .map { data -> data.toUiModel() }
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = UiModel.Loading
    )

SharingStarted 策略:

  • Eagerly:立即开始,无论是否有订阅者
  • Lazily:第一个订阅者出现时开始,永久保持活跃
  • WhileSubscribed(timeout):有订阅者时活跃,无订阅者后延迟 timeout 停止(最推荐)

5.4 在 Compose 中使用

kotlin 复制代码
@Composable
fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    // 处理一次性事件
    LaunchedEffect(Unit) {
        viewModel.events.collect { event ->
            when (event) {
                is ProfileEvent.ShowError -> {
                    snackbarHostState.showSnackbar(event.message)
                }
            }
        }
    }
    
    // 渲染 UI
    when {
        uiState.isLoading -> LoadingScreen()
        uiState.profile != null -> ProfileContent(uiState.profile!!)
    }
}

六、常见陷阱与解决方案

陷阱 1:StateFlow 的值比较陷阱

kotlin 复制代码
// 如果 UiState 没有正确实现 equals,可能导致不必要的重组
data class UiState(val items: List<Item>)  // ✅ data class 自动实现 equals

class UiState(val items: List<Item>)  // ❌ 类默认 equals 是引用比较

陷阱 2:SharedFlow 事件丢失

kotlin 复制代码
// ❌ 没有 replay,如果 UI 未准备好,事件会丢失
private val _events = MutableSharedFlow<Event>()

// ✅ 根据场景配置 replay(如果需要新订阅者收到最新事件)
private val _events = MutableSharedFlow<Event>(replay = 1)

陷阱 3:内存泄漏

kotlin 复制代码
// ❌ 在 Activity/Fragment 中直接 launch
lifecycleScope.launch {
    viewModel.state.collect { }  // 可能在后台继续运行
}

// ✅ 使用 repeatOnLifecycle
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.state.collect { }
    }
}

七、总结

StateFlow SharedFlow
本质 状态容器 事件广播
记忆 始终记住最新值 按配置回放
读取 支持同步读取 .value 仅支持异步收集
去重 自动去重 不去重
最佳场景 UI 状态管理 一次性事件、日志流

掌握 StateFlowSharedFlow 的区别与适用场景,是构建响应式、可维护 Android 应用的关键。记住这个核心原则:

StateFlow 用于状态(State),SharedFlow 用于事件(Event)。

在 MVVM 架构中,将两者结合使用,可以优雅地处理 UI 的所有数据流场景,写出更加清晰、健壮的代码。

相关推荐
盐烟1 小时前
xpath-csv_doban_slider
开发语言·python
小学生-山海1 小时前
【安卓逆向】WE Learn登录接口iv、pwd参数分析,加密逆向分析
开发语言·python·安卓逆向
Slow菜鸟1 小时前
Java 开发环境安装指南(7) | Nginx 安装
java·开发语言·nginx
沐苏瑶1 小时前
Java反序列化漏洞
java·开发语言·网络安全
进击的荆棘1 小时前
C++起始之路——用哈希表封装myunordered_set和myunordered_map
开发语言·c++·stl·哈希算法·散列表·unordered_map·unordered_set
心.c1 小时前
大厂高频手写题
开发语言·前端·javascript
guslegend2 小时前
AI生图第2节:python对接gpt-image-2模型API生图
开发语言·python·gpt
原来是猿2 小时前
Linux线程同步与互斥(四):日志系统与策略模式
linux·运维·开发语言·策略模式
卷心菜狗3 小时前
Python进阶--迭代器
开发语言·python