Kotlin 04Flow stateIn 和 shareIn的区别

一 Kotlin Flow 中的 stateIn 和 shareIn

一、简单比喻理解

想象一个水龙头(数据源)和几个水杯(数据接收者):

  • 普通 Flow(冷流):每个水杯来接水时,都要重新打开水龙头从头放水
  • stateIn/shareIn(热流):水龙头一直开着,水存在一个水池里,任何水杯随时来接都能拿到水

二、stateIn 是什么?

就像手机的状态栏

  • 总是显示最新的一条信息(有当前值
  • 新用户打开手机时,立刻能看到最后一条消息
  • 适合用来表示"当前状态",比如:
    • 用户登录状态(已登录/未登录)
    • 页面加载状态(加载中/成功/失败)
    • 实时更新的数据(如股票价格)

代码示例:

// 创建一个永远知道当前温度的温度计
val currentTemperature = sensorFlow
    .stateIn(
        scope = viewModelScope,  // 在ViewModel生命周期内有效
        started = SharingStarted.WhileSubscribed(5000), // 5秒无订阅就暂停
        initialValue = 0 // 初始温度0度
    )

// 在Activity中读取(总是能拿到当前温度)
textView.text = "${currentTemperature.value}°C"

三、shareIn 是什么?

就像广播电台

  • 不保存"当前值"(没有.value属性)
  • 新听众打开收音机时,可以选择:
    • 从最新的一条新闻开始听(replay=1)
    • 只听新新闻(replay=0)
  • 适合用来处理"事件",比如:
    • 显示Toast提示
    • 页面跳转指令
    • 一次性通知

代码示例:

// 创建一个消息广播站
val messages = notificationFlow
    .shareIn(
        scope = viewModelScope,
        started = SharingStarted.Lazily, // 有人收听时才启动
        replay = 1 // 新听众能听到最后1条消息
    )

// 在Activity中收听广播
lifecycleScope.launch {
    messages.collect { msg ->
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
    }
}

四、主要区别对比

特性stateIn (状态栏)shareIn (广播电台)
有无当前值有(.value 直接访问)无(必须通过collect接收)
新订阅者立即获得最新值可配置获得最近N条(replay)
典型用途持续更新的状态(如用户积分)一次性事件(如"购买成功"提示)
内存占用始终保存最新值按需缓存(可配置)
是否热流

五、为什么要用它们?

  1. 节省资源:避免重复计算(多个界面可以共享同一个数据源)

    • ❌ 不用时:每个界面都单独请求一次网络数据
    • ✅ 使用后:所有界面共享同一份网络数据
  2. 保持一致性:所有订阅者看到的数据完全相同

    • 比如用户头像更新后,所有界面立即同步
  3. 自动管理生命周期

    • 当Activity销毁时自动停止收集
    • 当配置变更(如屏幕旋转)时保持数据不丢失

六、生活场景类比

场景1:微信群(stateIn)

  • 群里最后一条消息就是当前状态(.value)
  • 新成员进群立刻能看到最后一条消息
  • 适合:工作群的状态同步

场景2:电台广播(shareIn)

  • 主播不断发送新消息
  • 听众打开收音机时:
    • 可以设置是否听之前的回放(replay)
    • 但无法直接问"刚才最后一首歌是什么"(无.value)
  • 适合:交通路况实时播报

七、什么时候用哪个?

用 stateIn 当:

  • 需要随时知道"当前值"
  • 数据会持续变化且需要被多个地方使用
  • 例如:
    • 用户登录状态
    • 购物车商品数量
    • 实时位置更新

用 shareIn 当:

  • 只关心新事件,不关心历史值
  • 事件可能被多个接收者处理
  • 例如:
    • "订单支付成功"通知
    • 错误提示消息
    • 页面跳转指令

八、超简单选择流程图

要管理持续变化的状态吗?
   是 → 需要直接访问当前值吗?
      是 → 用 stateIn
      否 → 用 shareIn(replay=1)
   否 → 这是一次性事件吗?
      是 → 用 shareIn(replay=0)

记住这个简单的口诀:
“状态用state,事件用share,想要回放加replay”

二 Kotlin Flow 的 shareInstateIn 操作符完全指南

在 Kotlin Flow 的使用中,shareInstateIn 是两个关键的操作符,用于优化流的共享和状态管理。本教程将深入解析这两个操作符的使用场景、区别和最佳实践。

一、核心概念解析

1. 冷流 vs 热流

  • 冷流 (Cold Flow):每个收集者都会触发独立的执行(如普通的 flow{} 构建器)
  • 热流 (Hot Flow):数据发射独立于收集者存在(如 StateFlowSharedFlow

2. 为什么需要 shareIn/stateIn?

  • 避免对上游冷流进行重复计算
  • 多个收集者共享同一个数据源
  • 将冷流转换为热流以提高效率

二、stateIn 操作符详解

基本用法

val sharedFlow: StateFlow<Int> = flow {
    // 模拟耗时操作
    emit(repository.fetchData())
}.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(),
    initialValue = 0
)

参数说明:

  • scope:共享流的协程作用域(通常用 viewModelScope
  • started:共享启动策略(后文详细讲解)
  • initialValue:必须提供的初始值

特点:

  1. 总是有当前值(通过 value 属性访问)
  2. 新收集者立即获得最新值
  3. 适合表示 UI 状态

使用场景示例:

用户个人信息状态管理

class UserViewModel : ViewModel() {
    private val _userState = repository.userUpdates() // Flow<User>
        .map { it.toUiState() }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = UserState.Loading
        )
    
    val userState: StateFlow<UserState> = _userState
}

三、shareIn 操作符详解

基本用法

val sharedFlow: SharedFlow<Int> = flow {
    emit(repository.fetchData())
}.shareIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(),
    replay = 1
)

参数说明:

  • replay:新收集者接收的旧值数量
  • extraBufferCapacity:超出 replay 的缓冲大小
  • onBufferOverflow:缓冲策略(SUSPEND, DROP_OLDEST, DROP_LATEST

特点:

  1. 可以有多个订阅者
  2. 没有 value 属性,必须通过收集获取数据
  3. 适合事件处理(如 Toast、导航事件)

使用场景示例:

全局事件通知

class EventBus {
    private val _events = MutableSharedFlow<Event>()
    val events = _events.asSharedFlow()
    
    suspend fun postEvent(event: Event) {
        _events.emit(event)
    }
    
    // 使用 shareIn 转换外部流
    val externalEvents = someExternalFlow
        .shareIn(
            scope = CoroutineScope(Dispatchers.IO),
            started = SharingStarted.Eagerly,
            replay = 0
        )
}

四、started 参数深度解析

1. SharingStarted.Eagerly

  • 行为:立即启动,无视是否有收集者
  • 用例:需要预先缓存的数据
  • 风险:可能造成资源浪费
started = SharingStarted.Eagerly

2. SharingStarted.Lazily

  • 行为:在第一个收集者出现时启动,保持活跃直到 scope 结束
  • 用例:长期存在的共享数据
  • 注意:可能延迟首次数据获取
started = SharingStarted.Lazily

3. SharingStarted.WhileSubscribed()

  • 行为
    • 有收集者时活跃
    • 最后一个收集者消失后保持一段时间(默认 0ms)
    • 可配置 stopTimeoutMillisreplayExpirationMillis
  • 用例:大多数 UI 相关状态
// 保留5秒供可能的重新订阅
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000)

五、关键区别对比

特性stateInshareIn
返回类型StateFlowSharedFlow
初始值必须提供无要求
新收集者获取立即获得最新 value获取 replay 数量的旧值
值访问通过 .value 直接访问必须通过收集获取
典型用途UI 状态管理事件通知/数据广播
背压处理总是缓存最新值可配置缓冲策略

六、最佳实践指南

1. ViewModel 中的标准模式

class MyViewModel : ViewModel() {
    // 状态管理用 stateIn
    val uiState = repository.data
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = null
        )
    
    // 事件处理用 shareIn
    val events = repository.events
        .shareIn(
            scope = viewModelScope,
            started = SharingStarted.Lazily,
            replay = 1
        )
}

2. 合理选择 started 策略

  • UI 状态WhileSubscribed(stopTimeoutMillis = 5000)
  • 配置变更需保留Lazily
  • 全局常驻数据Eagerly

3. 避免常见错误

错误1:在每次调用时创建新流

// 错误!每次调用都创建新流
fun getUser() = repository.getUserFlow()
    .stateIn(viewModelScope, SharingStarted.Eagerly, null)

// 正确:共享同一个流
private val _user = repository.getUserFlow()
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val user: StateFlow<User?> = _user

错误2:忽略 replay 配置

// 可能丢失事件
.shareIn(scope, SharingStarted.Lazily, replay = 0)

// 更安全的配置
.shareIn(scope, SharingStarted.Lazily, replay = 1)

七、高级应用场景

1. 结合 Room 数据库

@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun observeUsers(): Flow<List<User>>
}

// ViewModel 中
val users = userDao.observeUsers()
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(),
        initialValue = emptyList()
    )

2. 实现自动刷新功能

val autoRefreshData = flow {
    while(true) {
        emit(repository.fetchLatest())
        delay(30_000) // 每30秒刷新
    }
}.shareIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(),
    replay = 1
)

3. 多源数据合并

val combinedData = combine(
    repo1.data.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1),
    repo2.data.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
) { data1, data2 ->
    data1 + data2
}.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(),
    initialValue = emptyList()
)

八、性能优化技巧

  1. 合理设置 replay

    • UI 状态:replay = 1(确保新订阅者立即获得状态)
    • 事件通知:replay = 0(避免重复处理旧事件)
  2. 使用 WhileSubscribed 的过期策略

    started = SharingStarted.WhileSubscribed(
        stopTimeoutMillis = 5000,
        replayExpirationMillis = 60_000 // 1分钟后丢弃缓存
    )
    
  3. 避免过度缓冲

    .shareIn(
        scope = ...,
        replay = 1,
        extraBufferCapacity = 1, // 总共缓冲2个值
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    

九、测试策略

1. 测试 StateFlow

@Test
fun testStateFlow() = runTest {
    val testScope = TestScope()
    val flow = flowOf(1, 2, 3)
    
    val stateFlow = flow.stateIn(
        scope = testScope,
        started = SharingStarted.Eagerly,
        initialValue = 0
    )
    
    assertEquals(0, stateFlow.value) // 初始值
    
    testScope.advanceUntilIdle()
    assertEquals(3, stateFlow.value) // 最后发射的值
}

2. 测试 SharedFlow

@Test
fun testSharedFlow() = runTest {
    val testScope = TestScope()
    val flow = flowOf("A", "B", "C")
    
    val sharedFlow = flow.shareIn(
        scope = testScope,
        started = SharingStarted.Eagerly,
        replay = 1
    )
    
    val results = mutableListOf<String>()
    val job = launch {
        sharedFlow.collect { results.add(it) }
    }
    
    testScope.advanceUntilIdle()
    assertEquals(listOf("A", "B", "C"), results)
    
    job.cancel()
}

十、总结决策树

何时使用 stateIn

  • 需要表示当前状态(有 .value 属性)
  • UI 需要立即访问最新值
  • 适合:页面状态、表单数据、加载状态

何时使用 shareIn

  • 处理一次性事件
  • 需要自定义缓冲策略
  • 适合:Toast 消息、导航事件、广播通知

选择哪种 started 策略?

  • WhileSubscribed():大多数 UI 场景
  • Lazily:配置变更需保留数据
  • Eagerly:需要预加载的全局数据

通过本教程,应该已经掌握了 shareInstateIn 的核心用法和高级技巧。正确使用这两个操作符可以显著提升应用的性能和资源利用率。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值