4. 使用 stateIn 替代手动 collect:原理与实现

手动 collect 每次订阅都会重新触发上游 Flow 的生产逻辑,导致重复计算和资源浪费。通过 stateIn 操作符将 Flow 转换为 StateFlow,可以缓存最新值、共享数据流,从而显著减少不必要的 collect 耗电。

4.1 核心原理

stateIn 在给定的 CoroutineScope 内启动一个共享的协程,持续收集上游 Flow 的最新值,并将其缓存到 StateFlow 中。多个订阅者通过 StateFlow.value 获取当前状态,无需重新触发上游生产逻辑。

  • 缓存最新值:StateFlow 始终保留最近一次发射的值,即使订阅者暂时断开,重新订阅时也能立即获取缓存值,避免重新执行耗电操作。
  • 共享上游:所有订阅者共享同一个上游协程,上游只运行一次。
  • 生命周期感知:通过 SharingStarted 策略控制上游何时开始、停止,降低无效运作。
💡 提示:stateIn 适用于“状态”型数据(如 UI 状态),而 shareIn 适用于“事件”型数据(如一次性导航事件)。

4.2 实现流程

  1. 选择 SharingStarted 策略:决定上游启动与停止的时机。
  2. 创建 StateFlow:在 ViewModel 或数据层中使用 stateIn 绑定 scope、策略和初始值。
  3. 替换手动 collect:UI 层通过 .collectAsState() 或 .value 读取状态,不再手动启动收集协程。
  4. 取消旧订阅:移除原有的 viewModelScope.launch { ... } 模板代码。

4.3 缓存策略对比

策略 启动时机 停止时机 适用场景
WhileSubscribed(timeoutMillis) 第一个订阅者出现时 最后一个订阅者消失后 timeout 秒 UI 可见时收集,短暂不可见(例如旋转屏幕)保留缓存,避免频繁重启
Eagerly 立即启动(无视订阅者) scope 取消时停止 数据需要始终最新,即使 UI 不在前台(如位置更新)
Lazily 第一个订阅者出现时 scope 取消时停止 只会在有订阅时消耗资源,适合非实时数据
⚠ 注意:WhileSubscribed(0) 会在最后订阅者消失时立即停止上游,可能导致频繁重启,反而增加耗电。推荐使用 3000–5000 毫秒的超时值。

4.4 代码示例:使用 stateIn 替代手动 collect

class LocationViewModel : ViewModel() {
    private val locationDao: LocationDao
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    // 以前:手动 collect(多次订阅触发多次位置更新)
    // fun startTracking() {
    //     viewModelScope.launch {
    //         locationDao.getLocationFlow().collect { location ->
    //             _uiState.value = UiState(location)
    //         }
    //     }
    // }

    // 现在:使用 stateIn 共享上游
    val locationState: StateFlow<Location?> = locationDao.getLocationFlow()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = null
        )

    // UI 层只需调用 collectAsState(),不再手动 launch
}

4.5 避免重复订阅

当多个 Fragment 或 Compose 需要监听同一数据源时,手动 collect 会为每个订阅者启动一个独立的上游协程,导致 N 倍耗电。而 stateIn 让所有订阅者共享协程,上游只运行一次:

// ❌ 错误:每个 collect 都触发独立上游
viewModelScope.launch { flow.collect { ... } }
viewModelScope.launch { flow.collect { ... } }   // 重复订阅

// ✅ 正确:只保留一个 stateIn 转换,UI 层复用
val sharedState = flow.stateIn(viewModelScope, WhileSubscribed(5000), ...)

4.6 常见错误

  • 忘记指定 scope:stateIn 必须传入 CoroutineScope,否则默认使用空 scope 导致上游永不启动。
  • 错误使用 WhileSubscribed(0):导致上游频繁启动/停止,增加功耗。
  • 混淆 stateIn 与 shareIn:shareIn 用于事件流,不缓存最新值;stateIn 用于状态流。

4.7 调试技巧

通过 SharingStarted.WhileSubscribed(5000) 的源码观察订阅计数:

// 在日志中打印 subscriptionCount
val flow = locationDao.getLocationFlow()
    .onStart { Log.d("StateIn", "上游启动") }
    .onCompletion { Log.d("StateIn", "上游停止") }
    .stateIn(viewModelScope, WhileSubscribed(5000), null)

4.8 性能与稳定性优化

  • 超时时间调优:根据场景调整 WhileSubscribed 的 timeout(推荐 3000~8000 ms),平衡缓存可用性与资源释放。
  • 避免冷启动延迟:对于 UI 初始化阶段必须立即展示的数据,使用 Eagerly 或手动触发上游预热。
  • 减少初始值开销:initialValue 避免使用复杂对象,防止内存泄漏。
stateIn 生命流程与缓存策略对比 上游 Flow stateIn scope + strategy + initial StateFlow Collector A Collector B 所有订阅者消失 超时控制停止 WhileSubscribed(5s) 订阅归零后等待5s再停止 Eagerly 立即启动,scope 取消才停止 Lazily 有订阅才启动,scope 取消停止

4.9 总结

stateIn 通过缓存最新值、共享上游协程和灵活的生命周期策略,从根本上避免了手动 collect 带来的重复订阅与不必要计算。选择合适的 SharingStarted 策略和超时时间,可以精确控制资源消耗,是 Android 流量优化的核心手段。