Android Data 层 Flow 最佳实践:以冷流为基础,按需转热,避免过早共享状态

1 阅读8分钟

在 Android 项目中,Flow 已经成为 Data 层、Domain 层和 UI 层之间传递异步数据的主流方式。但一个常见问题是:Repository 或 DataSource 到底应该暴露 FlowStateFlow,还是 SharedFlow

一个实用的原则是:

Data 层冷流为主,热流为辅;不要在 Data 层过早 stateIn / shareIn

也就是说,Data 层默认暴露 Flow<T>。只有在确实需要共享状态、缓存最新值或维持长期数据源时,才考虑暴露 StateFlow / SharedFlow

多数情况下,stateIn / shareIn 应该放在 ViewModel 或更明确的生命周期边界中,而不是提前放进 Repository。


1. 为什么 Data 层优先使用冷流

冷流 Flow 的特点是:只有在被收集时才执行,上游生命周期由收集者控制。

这非常适合 Data 层,因为 Repository 和 DataSource 通常只负责描述“数据从哪里来”,而不应该过早决定:

  • 数据何时启动
  • 数据何时停止
  • 数据缓存多久
  • 数据由哪个 CoroutineScope 承载
  • 多个订阅者之间是否共享上游

例如 Room DAO 本身就是典型冷流:

image.png

Repository 只需要做数据转换:

image.png

这里不需要在 Repository 中调用 stateInshareIn

因为 stateIn / shareIn 并不是普通的“类型转换”,它们会把冷流变成热流,并引入新的行为:

  • 需要一个 CoroutineScope
  • 可能让上游提前启动
  • 可能让上游在没有订阅者时仍然存活
  • 可能缓存最后一次数据
  • 可能改变多订阅者行为
  • 可能让资源释放时机变得不直观

所以,除非你非常明确地需要这些行为,否则不要在 Data 层提前使用它。

2. stateIn / shareIn 应该放在哪里

通常建议:

image.png

也就是说,Data 层先提供冷流,ViewModel 再根据 UI 的生命周期和状态需求转换成 StateFlow

image.png

UI 中可以这样使用:

image.png

这种方式的好处是:

  • UI 需要状态,ViewModel 提供状态
  • viewModelScope 是明确的生命周期边界
  • SharingStarted.WhileSubscribed(5_000) 与界面订阅关系清晰
  • Data 层保持轻量、可组合、易测试
  • Repository 不需要持有额外的 CoroutineScope

3. 不要把 stateIn / shareIn 当成性能优化默认项

有些代码会在 Repository 中提前做这样的处理:

image.png

这看起来像是“提前缓存一份数据”,但实际引入了很多隐藏成本:

  • SharingStarted.Eagerly 会让上游立即启动
  • Repository 需要一个外部传入的 CoroutineScope
  • 数据流生命周期脱离具体界面
  • 上游可能在没有 UI 使用时仍然工作
  • 容易把 Repository 变成状态容器

更推荐的写法是上面 2 的写法.

不要为了“看起来更方便”而提前 stateIn
它会让数据流从“按需执行”变成“被某个 scope 托管的常驻状态”。


4. Data 层什么时候可以使用热流

Data 层不是不能使用热流,而是不要默认使用热流。

适合在 Data 层暴露热流的场景包括:

  • 应用级登录状态
  • WebSocket 长连接状态
  • 蓝牙、定位、传感器等共享状态
  • 多个消费者需要复用同一个昂贵上游
  • 需要缓存最新值
  • 内部事件分发

例如登录状态可以设计成:

image.png

消息流可以设计成:

image.png

但这里的重点是:热流应该来自明确的需求,而不是来自习惯。

你可以用下面几个问题判断是否应该在 Data 层 stateIn / shareIn

  • 这个数据是否天然是应用级共享状态?
  • 没有 UI 订阅时,它是否仍然应该继续存在?
  • 是否多个消费者必须共享同一个上游连接?
  • 是否确实需要缓存最新值?
  • 这个 CoroutineScope 的生命周期是否非常明确?

如果这些问题回答不清楚,就不要提前转热流。


5. 广播监听示例:用 callbackFlow 暴露冷流

广播监听很适合用 callbackFlow 包装成冷流。

下面是一个监听网络状态变化的示例:

image.png

状态定义:

image.png

Repository 继续暴露冷流,不要在这里提前 stateIn

image.png

ViewModel 再转成 StateFlow

image.png

awaitClose 非常关键,它保证当 Flow 不再被收集时,广播接收器会被注销,避免资源泄漏。

如果 Repository 提前 shareIn,广播接收器的注册和注销时机就会受到 Repository 内部 scope 和 sharing 策略影响,排查问题会更困难。


6. ContentProvider 示例:用 ContentObserver 监听内容变化

除了广播,ContentProvider 也是 Data 层常见的数据来源,例如系统联系人、媒体库、日历,或业务 App 自己暴露的数据。

这类数据通常通过 ContentResolver 查询,并通过 ContentObserver 监听变化。它同样适合包装成冷流:

image.png

对应的数据模型:

image.png

Repository 继续暴露冷流:

image.png ViewModel 再根据 UI 需要转成 StateFlow

image.png

UI 中收集:

image.png

这个示例和广播示例的设计思路是一致的:Data 层依然暴露冷流,而不是直接暴露 StateFlow

原因是:

  • 只有被收集时才注册 ContentObserver
  • 停止收集时自动注销监听
  • 不需要 Data 层持有 CoroutineScope
  • 生命周期交给 ViewModel / UI 控制
  • 查询逻辑更容易测试和替换

最关键的是这段:

image.png

它保证 Flow 结束收集时释放 ContentObserver,避免监听泄漏。


7. callbackFlow 的统一模式

无论是广播还是 ContentProvider,它们都属于系统回调式 API。

共同特点是:

  • 需要注册监听器
  • 需要取消注册
  • 可能需要发送初始值
  • 适合包装成冷流
  • 需要用 awaitClose 释放资源

统一模式可以写成:

image.png

然后在 Repository 中继续保持冷流,不要提前 stateIn / shareIn

image.png

最后在 ViewModel 中转换成 UI 所需的 StateFlow

image.png


8. 推荐的分层规则

image.png

这个规则可以让每一层职责更稳定:

  • DataSource 关注原始数据来源
  • Repository 关注业务数据聚合与转换
  • ViewModel 关注 UI 状态
  • UI 关注展示和交互

9. 总结

Android Data 层设计 Flow 时,最重要的原则是:

// Data 层默认
Flow<T>

// ViewModel 默认
StateFlow<UiState>

// Data 层少数共享状态或事件场景
StateFlow<T>
SharedFlow<Event>

再强调一次:

不要在 Data 层过早 stateIn / shareIn

stateIn / shareIn 会改变 Flow 的执行模型,把“按需执行的冷流”变成“由某个 scope 托管的热流”。这会引入生命周期、缓存、共享和资源释放问题。

冷流让 Data 层保持轻量、可组合、易测试;热流则适合表达共享状态、缓存状态和持续事件。

因此,不要在 Repository 中为了方便而提前把所有数据都转成 StateFlow。让 Data 层先提供清晰的冷流,再由 ViewModel 根据 UI 生命周期和状态需求进行转换,通常会得到更简单、更稳定、也更符合 Android 架构边界的设计。