【Kotlin 语法知识】Kotlin 中"Flow"的几个概念: StateFlow, SharedFlow, 冷流, 热流

0 阅读7分钟

由于缺乏热情,他很自然地把所下的决心都慢慢淡忘了。有一天他没去医院,第二天他没去上课;闲散的滋味使人贪恋,他慢慢就完全不去学习了。——《包法利夫人》

1_c3nbEdjnk0_Re34Q_tm6Bw.png

处理异步数据流:StateFlow 与 SharedFlow

这两者都是 Kotlin 协程 Flow API 的一部分,用来处理异步数据流,将数据发射出去,供下游监听。它们的主要区别在于 应用场景不同

我们知道在 Android 页面实现的过程中,所传输的对象分为 状态控制 两种,状态 标识一个页面当前的 UI 数据,是一个 长期阶段性 的概念;而 控制 则是 引起可见/不可见变化的事件状态持续的,可以获取到任何时间点所处的状态,反过来,任何时间点一定处于某个状态。而 控制瞬时的,获取上一次控制是没有意义的,因为它只存在于发生的那一个瞬间。

这两种概念,就分别对应着 StateFlow(状态流)与 SharedFlow(控制流)。

StateFlow 状态流

1. 含义

  • 状态容器: 专门用于管理 需要持续观察的 UI 状态(如页面数据、加载状态等)。
  • 特性:
    • 必选初始值: 必须指定初始状态。
    • 仅保留最新值: 始终持有最新状态,新订阅者会立即收到当前值。
    • 状态去重: 若新值与旧值相同(通过 equals 判断),则不会触发更新。

2. 应用场景

  • UI 状态管理: 例如用户信息、列表数据、加载状态(Loading/Success/Error)。
  • 需要持久化观察的数据: 如主题模式、语言设置等 全局状态
  • 替代 LiveData:纯协程架构中,更推荐使用 StateFlow(需结合 Lifecycle.repeatOnLifecycle 避免内存泄漏)。

3. 代码示例

// ViewModel 中定义
private val _uiState = MutableStateFlow<DataState>(DataState.Loading)
val uiState: StateFlow<DataState> = _uiState

// 更新状态
fun loadData() {
    viewModelScope.launch {
        _uiState.value = DataState.Loading
        try {
            val data = repository.fetchData()
            _uiState.value = DataState.Success(data)
        } catch (e: Exception) {
            _uiState.value = DataState.Error(e.message)
        }
    }
}

注意这里赋值时使用 _uiState.value = DataState.Success(data)等号 进行赋值

SharedFlow 控制流

1. 含义

  • 事件分发器: 用于处理 一次性事件广播事件(如错误提示、导航事件)。
  • 特性:
    • 无初始值: 不需要初始值。
    • 灵活配置: 通过参数控制事件的重放(replay)和缓存(buffer)策略。
    • 多订阅者支持: 所有订阅者共享同一控制流。

2. 应用场景

  • 一次性事件: 如显示 ToastSnackbar、导航到其他页面。
  • 高频事件: 如搜索框的 实时输入联想(需防抖处理)。
  • 广播通知: 如网络状态变化、权限变更等 全局事件

3. 代码示例

// ViewModel 中定义
private val _toastEvent = MutableSharedFlow<String>()
val toastEvent: SharedFlow<String> = _toastEvent

// 发射事件
fun showToast(message: String) {
    viewModelScope.launch {
        _toastEvent.emit(message)
    }
}

注意这里发射时使用了 _toastEvent.emit(message),而在处理 StateFlow 时则是使用 =,关于两者的区别,将在本文后半部分进行说明。

StateFlow 本质上是 SharedFlow 的封装

  • 重放次数 0 -> 1
  • 无初始值 -> 具备初始值
  • 无去重 -> 去重(通过 equals 判断)
// StateFlow 等效于以下配置的 SharedFlow:
val stateFlow = MutableSharedFlow(
  replay = 1,
  onBufferOverflow = BufferOverflow.DROP_OLDEST
)
stateFlow.tryEmit(initialValue) // 设置初始值

StateFlow 与 SharedFlow 对比表格

特性StateFlowSharedFlow
初始值必须提供不需要
值更新策略仅保留最新值(类似 ConflatedChannel)可配置缓存和重放策略
事件去重自动去重(基于 equals)无自动去重
适用场景持久化状态管理一次性事件或高频事件

选择策略

  1. 用 StateFlow 的场景:
    • 需要管理 UI 状态(如用户信息、页面数据)。
    • 希望新订阅者立即获取最新状态。
    • 需要避免重复状态更新(如防止界面闪烁)。
  2. 用 SharedFlow 的场景:
    • 处理一次性事件(如 Toast、导航)。
    • 需要广播事件给多个订阅者。
    • 需要自定义事件缓存(如保留最近 N 个事件)。

MutableStateFlow 的 emit 与 update 函数

这两个函数都是用来更新值,以便下游监听变化,在使用场景上有些微的区别。

1. emit 函数

  • 作用: 直接设置一个新的值,覆盖当前值。
  • 特点:
    • 直接替换当前值,不依赖旧值。
    • 是挂起函数(需要协程作用域调用)。
    • 等同于直接设置 value 属性(如 stateFlow.value = newValue)。
  • 使用场景:
    • 当新值与旧值无关时(如直接设置固定值、用户输入等)。
    • 在协程作用域中更新状态。
  • 示例:
// 直接发射新值
stateFlow.emit(100)
// 或非挂起环境下:
stateFlow.value = 100

2. update 函数

  • 作用: 基于当前值进行原子性更新。
  • 特点:
    • 通过 Lambda 接收当前值,返回新值。
    • 保证原子性操作(避免并发修改导致竞态条件)。
    • 自动处理值更新,确保每次计算基于最新的状态。
  • 使用场景:
    • 当新值依赖旧值时(如递增、递减、复合状态修改)。
    • 需要线程安全更新的场景(多协程/线程同时修改状态)。
  • 示例:
// 原子性递增
stateFlow.update { current -> current + 1 }

emit 与 update 对比表格

对比项emitupdate
依赖旧值不需要旧值,直接设置新值。需要基于旧值计算新值。
原子性非原子操作(直接覆盖)。原子操作(确保并发安全)。
适用场景简单赋值(如重置状态、用户输入)。依赖旧值的复杂更新(如计数器)。

冷流 Cold Flow 与热流 Hot Flow

冷流、热流 是 Kotlin 协程中 Flow 的两种不同类型,冷流 是最常见的,通过 flow{...} 即可以启动,其特点是每次收集时都会启动新的数据生产,即 每个收集者都会从头开始接收数据

热流StateFlowSharedFlow 都属于热流)的数据生产过程,则是独立于收集过程存在的。如果有多个收集者,则它们共享一个数据流,不会各自触发新的数据生产。

1、冷流 Cold Flow

  • 定义: 冷流是 惰性数据流,数据生产在每次被收集(collect)时触发,且每个收集者会获得独立的完整数据序列。
    • 类似场景: 像在抖音播放视频,每次打开都从头开始播放完整的一条视频。
    • 常见实现: 通过 flow { ... } 构建的普通 Flow。
  • 核心特点:
    • 按需触发: 没有收集者时,数据生产代码不会执行。
    • 独立数据序列: 每个收集者触发独立的数据生产流程。
    • 无共享状态: 数据生产是“干净”的,不同收集者之间互不影响。
    • 适合一次性操作: 如网络请求、数据库查询等。
  • 适用场景:
    • 需要每次收集时重新获取数据(如实时搜索输入的关键字)。
    • 执行一次性任务(如单次网络请求)。
    • 数据生产开销较大,需确保只在需要时执行。
  • 注意事项:
    • 冷流的重复开销,频繁收集冷流可能导致重复执行耗时操作(如多次网络请求),需结合 cachedInshareIn 优化。
  • 示例:
// 冷流:每次 collect 时触发新的数据生产
val coldFlow = flow {
  repeat(3) { i ->
    delay(100)
    emit("冷流值 $i")
  }
}

// 收集者 1
coldFlow.collect { println("收集者1: $it") } // 输出 0,1,2
// 收集者 2(再次收集时重新生产数据)
coldFlow.collect { println("收集者2: $it") } // 输出 0,1,2

2、热流

  • 定义: 热流是 主动数据流,数据生产与收集者无关,即使没有收集者,数据也会被生产并缓存(取决于配置)。多个收集者共享同一数据源。
    • 类似场景: 像斗鱼直播,无论是否有观众,内容都会持续推送。
    • 常见实现: StateFlowSharedFlow,或通过 stateIn/shareIn 转换后的 Flow。
  • 核心特点:
    • 主动生产数据: 数据生产在流创建时启动,与收集者无关。
    • 数据共享: 多个收集者共享同一数据源,后续收集者可能收到历史数据(取决于配置)。
    • 有状态: 通常用于持续的状态更新或事件广播。
    • 适合实时场景: 如 UI 状态管理、事件通知。
  • 适用场景:
    • 需要多个订阅者共享同一数据源(如全局状态管理)。
    • 持续的事件流(如传感器数据、实时消息推送)。
    • 需要缓存历史数据供新订阅者使用(如保留最后 N 个事件)。
  • 注意事项:
    • 热流的生命周期管理,热流的数据生产可能长期存活,需通过协程作用域(如 viewModelScope)控制其生命周期,避免内存泄漏。
  • 示例:
// 冷流转热流:通过 shareIn 共享数据生产
val hotFlow = coldFlow.shareIn(
  scope = CoroutineScope(Dispatchers.Default),
  started = SharingStarted.WhileSubscribed(),
  replay = 1 // 新订阅者收到最近 1 个历史值
)

// 收集者 1(立即开始接收数据)
hotFlow.collect { println("收集者1: $it") } // 可能输出 0,1,2(取决于生产速度)
delay(150) // 延迟后收集者 2 加入
// 收集者 2(收到最近 1 个历史值 + 后续新值)
hotFlow.collect { println("收集者2: $it") } // 可能输出 1,2

3、冷流、热流对比表格

特性冷流(Cold Flow)热流(Hot Flow)
数据生产触发时机每次收集时触发新的生产独立于收集者,主动生产
数据共享每个收集者获得独立数据序列多个收集者共享同一数据源
资源开销每次收集都可能重复触发生产(可能浪费)单次生产,多个消费者共享(高效)
初始数据总是从头开始新收集者可能收到历史数据(可配置)
典型场景一次性任务、按需请求持续状态更新、事件广播
常见实现flow { ... }asFlow()StateFlowSharedFlow

4、冷流 vs 热流互相转换

  • 冷流 -> 热流

通过 shareInstateIn 操作符将冷流转换为热流,实现数据共享:

// shareIn:转换为 SharedFlow
val hotSharedFlow = coldFlow.shareIn(
  scope = viewModelScope,
  started = SharingStarted.WhileSubscribed(5000), // 无订阅者时保留 5 秒
  replay = 1
)

// stateIn:转换为 StateFlow
val hotStateFlow = coldFlow.stateIn(
  scope = viewModelScope,
  started = SharingStarted.WhileSubscribed(),
  initialValue = "初始值"
)
  • 热流 -> 冷流

使用较少,一般直接创建冷流即可。

通过 callbackFlowchannelFlow 将热数据源包装为冷流:

fun hotToCold(): Flow<Data> = callbackFlow {
  val listener = { data: Data -> trySend(data) }
  registerHotSource(listener)
  awaitClose { unregisterHotSource(listener) }
}