SharedFlow vs StateFlow 与 冷流 vs 热流:深入解析与示例

SharedFlow(replay = 1) vs StateFlow

虽然 SharedFlow(replay = 1)StateFlow 看起来很相似,但它们有重要区别:

特性SharedFlow(replay = 1)StateFlow
初始值不需要初始值必须提供初始值
值相等性总是发射所有值跳过与当前值相等的值
空值处理可以发射 null初始值不能为 null
重放机制明确指定重放数量固定重放最新值
值更新所有值都会触发收集只有不同值才会触发
适用场景事件、通知状态管理
转换关系更基础的原语本质上是 SharedFlow(replay=1) 的特殊实现

代码示例对比

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    // SharedFlow(replay=1) 示例
    val sharedFlow = MutableSharedFlow<Int>(replay = 1)

    launch {
        repeat(5) { i ->
            sharedFlow.emit(i)
            delay(100)
        }
    }

    delay(250)
    println("SharedFlow 收集:")
    sharedFlow.collect { println("  收集到: $it") }

    // StateFlow 示例
    val stateFlow = MutableStateFlow(-1) // 必须初始值

    launch {
        repeat(5) { i ->
            stateFlow.value = i
            delay(100)
        }
    }

    delay(250)
    println("StateFlow 收集:")
    stateFlow.collect { println("  收集到: $it") }
}

输出:

SharedFlow 收集:
  收集到: 2   // 最近发射的值
  收集到: 3
  收集到: 4

StateFlow 收集:
  收集到: -1  // 初始值
  收集到: 2   // 最近状态值
  收集到: 3
  收集到: 4

关键区别分析

  1. 初始值处理
  • StateFlow 强制要求初始值,收集者首先收到初始值
  • SharedFlow 没有初始值,新收集者只收到重放的值
  1. 值相等性检查
   // SharedFlow - 总是发射
   sharedFlow.emit(1) // 收集者收到1
   sharedFlow.emit(1) // 收集者再次收到1

   // StateFlow - 跳过相同值
   stateFlow.value = 1 // 收集者收到1
   stateFlow.value = 1 // 无变化,不触发收集
  1. null 处理
   // SharedFlow 支持 null
   val shared = MutableSharedFlow<String?>(replay=1)
   shared.emit(null) // 有效

   // StateFlow 初始值不能为 null
   val state = MutableStateFlow(null) // 编译错误!

冷流 vs 热流:概念与示例

冷流 (Cold Flow)

定义

  • 每个收集者触发独立的流执行
  • 类似 DVD – 每次播放从头开始
  • 数据生产是惰性的(有收集者才开始)
  • 适合一次性数据操作

特性

  • 每个收集者有独立数据序列
  • 无状态,不保存历史
  • 资源消耗与收集者数量相关
  • 典型实现:flow { } 构建器

示例场景

  1. 网络请求
  2. 数据库查询
  3. 文件读取

代码示例

val coldFlow = flow {
    println("开始执行")
    emit(1)
    emit(2)
    emit(3)
}

fun main() = runBlocking {
    // 第一次收集
    coldFlow.collect { println("收集者A: $it") }

    // 第二次收集(重新执行)
    coldFlow.collect { println("收集者B: $it") }
}

输出

开始执行
收集者A: 1
收集者A: 2
收集者A: 3
开始执行
收集者B: 1
收集者B: 2
收集者B: 3

热流 (Hot Flow)

定义

  • 数据发射独立于收集者存在
  • 类似电视直播 – 随时加入观看
  • 数据生产立即开始
  • 多个收集者共享相同数据流

特性

  • 收集者共享同一数据序列
  • 有状态,可保留历史数据
  • 资源消耗与收集者数量无关
  • 典型实现:SharedFlow, StateFlow

示例场景

  1. 用户位置更新
  2. 实时股价推送
  3. 应用状态管理
  4. 全局事件总线

代码示例

val hotFlow = MutableSharedFlow<Int>(replay = 1)

fun main() = runBlocking {
    // 开始发射数据(无收集者)
    launch {
        repeat(5) { i ->
            hotFlow.emit(i)
            delay(100)
        }
    }

    delay(250) // 等待发射部分数据

    // 第一个收集者
    launch {
        hotFlow.collect { println("收集者A: $it") }
    }

    delay(200)

    // 第二个收集者
    launch {
        hotFlow.collect { println("收集者B: $it") }
    }
}

输出

收集者A: 2   // 加入时收到当前值
收集者A: 3
收集者A: 4
收集者B: 4   // 新收集者收到最新值

冷流 vs 热流对比表

特性冷流热流
数据生产时机收集时开始创建时开始
数据共享每个收集者独立所有收集者共享
历史数据可保留
资源消耗与收集者数量成正比固定
状态保持无状态有状态
典型用例一次性操作持续状态/事件
创建方式flow { }MutableSharedFlow
转换方法使用 shareIn使用 onStart

实际应用场景示例

冷流用例:API 请求

fun fetchUserData(userId: String): Flow<User> = flow {
    // 每次收集触发新请求
    val response = apiService.getUser(userId)
    emit(response.toUser())
}

// 使用
viewModelScope.launch {
    fetchUserData("123").collect { user ->
        updateUI(user)
    }
}

热流用例:实时位置追踪

class LocationService {
    private val _locations = MutableSharedFlow<Location>(replay = 1)
    val locations: SharedFlow<Location> = _locations

    init {
        startLocationUpdates { location ->
            _locations.tryEmit(location)
        }
    }
}

// 多个UI组件使用
class MapFragment {
    locationService.locations.collect { /* 更新地图 */ }
}

class StatusBar {
    locationService.locations.collect { /* 显示位置状态 */ }
}

混合用例:缓存策略

fun getData(): Flow<Data> {
    // 冷流:首次从网络加载
    val networkFlow = flow {
        val data = fetchFromNetwork()
        emit(data)
    }

    // 热流:缓存最新值
    return networkFlow.shareIn(
        viewModelScope,
        SharingStarted.WhileSubscribed(),
        replay = 1
    )
}

如何选择

  1. 选择冷流当
  • 需要按需触发操作(如按钮点击)
  • 数据是短暂的一次性操作
  • 不同收集者需要独立数据
  1. 选择热流当
  • 需要持续观察变化(如数据库)
  • 多个组件需要共享相同状态
  • 需要保留历史数据
  • 事件驱动架构(如全局通知)
  1. 选择 SharedFlow 当
  • 需要事件总线
  • 需要自定义重放策略
  • 需要发射 null 值
  • 需要所有值都触发收集
  1. 选择 StateFlow 当
  • 管理UI状态
  • 需要初始值
  • 自动跳过重复值
  • 简单状态管理场景

理解这些概念的区别对于设计高效、响应式的Kotlin应用程序至关重要。根据具体需求选择合适的流类型,可以显著提升应用性能和代码可维护性。