在 Android 项目中,Flow 已经成为 Data 层、Domain 层和 UI 层之间传递异步数据的主流方式。但一个常见问题是:Repository 或 DataSource 到底应该暴露 Flow、StateFlow,还是 SharedFlow?
一个实用的原则是:
Data 层冷流为主,热流为辅;不要在 Data 层过早
stateIn/shareIn。
也就是说,Data 层默认暴露 Flow<T>。只有在确实需要共享状态、缓存最新值或维持长期数据源时,才考虑暴露 StateFlow / SharedFlow。
多数情况下,stateIn / shareIn 应该放在 ViewModel 或更明确的生命周期边界中,而不是提前放进 Repository。
1. 为什么 Data 层优先使用冷流
冷流 Flow 的特点是:只有在被收集时才执行,上游生命周期由收集者控制。
这非常适合 Data 层,因为 Repository 和 DataSource 通常只负责描述“数据从哪里来”,而不应该过早决定:
- 数据何时启动
- 数据何时停止
- 数据缓存多久
- 数据由哪个
CoroutineScope承载 - 多个订阅者之间是否共享上游
例如 Room DAO 本身就是典型冷流:
Repository 只需要做数据转换:
这里不需要在 Repository 中调用 stateIn 或 shareIn。
因为 stateIn / shareIn 并不是普通的“类型转换”,它们会把冷流变成热流,并引入新的行为:
- 需要一个
CoroutineScope - 可能让上游提前启动
- 可能让上游在没有订阅者时仍然存活
- 可能缓存最后一次数据
- 可能改变多订阅者行为
- 可能让资源释放时机变得不直观
所以,除非你非常明确地需要这些行为,否则不要在 Data 层提前使用它。
2. stateIn / shareIn 应该放在哪里
通常建议:
也就是说,Data 层先提供冷流,ViewModel 再根据 UI 的生命周期和状态需求转换成 StateFlow。
UI 中可以这样使用:
这种方式的好处是:
- UI 需要状态,ViewModel 提供状态
viewModelScope是明确的生命周期边界SharingStarted.WhileSubscribed(5_000)与界面订阅关系清晰- Data 层保持轻量、可组合、易测试
- Repository 不需要持有额外的
CoroutineScope
3. 不要把 stateIn / shareIn 当成性能优化默认项
有些代码会在 Repository 中提前做这样的处理:
这看起来像是“提前缓存一份数据”,但实际引入了很多隐藏成本:
SharingStarted.Eagerly会让上游立即启动- Repository 需要一个外部传入的
CoroutineScope - 数据流生命周期脱离具体界面
- 上游可能在没有 UI 使用时仍然工作
- 容易把 Repository 变成状态容器
更推荐的写法是上面 2 的写法.
不要为了“看起来更方便”而提前 stateIn。
它会让数据流从“按需执行”变成“被某个 scope 托管的常驻状态”。
4. Data 层什么时候可以使用热流
Data 层不是不能使用热流,而是不要默认使用热流。
适合在 Data 层暴露热流的场景包括:
- 应用级登录状态
- WebSocket 长连接状态
- 蓝牙、定位、传感器等共享状态
- 多个消费者需要复用同一个昂贵上游
- 需要缓存最新值
- 内部事件分发
例如登录状态可以设计成:
消息流可以设计成:
但这里的重点是:热流应该来自明确的需求,而不是来自习惯。
你可以用下面几个问题判断是否应该在 Data 层 stateIn / shareIn:
- 这个数据是否天然是应用级共享状态?
- 没有 UI 订阅时,它是否仍然应该继续存在?
- 是否多个消费者必须共享同一个上游连接?
- 是否确实需要缓存最新值?
- 这个
CoroutineScope的生命周期是否非常明确?
如果这些问题回答不清楚,就不要提前转热流。
5. 广播监听示例:用 callbackFlow 暴露冷流
广播监听很适合用 callbackFlow 包装成冷流。
下面是一个监听网络状态变化的示例:
状态定义:
Repository 继续暴露冷流,不要在这里提前 stateIn:
ViewModel 再转成 StateFlow:
awaitClose 非常关键,它保证当 Flow 不再被收集时,广播接收器会被注销,避免资源泄漏。
如果 Repository 提前 shareIn,广播接收器的注册和注销时机就会受到 Repository 内部 scope 和 sharing 策略影响,排查问题会更困难。
6. ContentProvider 示例:用 ContentObserver 监听内容变化
除了广播,ContentProvider 也是 Data 层常见的数据来源,例如系统联系人、媒体库、日历,或业务 App 自己暴露的数据。
这类数据通常通过 ContentResolver 查询,并通过 ContentObserver 监听变化。它同样适合包装成冷流:
对应的数据模型:
Repository 继续暴露冷流:
ViewModel 再根据 UI 需要转成
StateFlow:
UI 中收集:
这个示例和广播示例的设计思路是一致的:Data 层依然暴露冷流,而不是直接暴露 StateFlow。
原因是:
- 只有被收集时才注册
ContentObserver - 停止收集时自动注销监听
- 不需要 Data 层持有
CoroutineScope - 生命周期交给 ViewModel / UI 控制
- 查询逻辑更容易测试和替换
最关键的是这段:
它保证 Flow 结束收集时释放 ContentObserver,避免监听泄漏。
7. callbackFlow 的统一模式
无论是广播还是 ContentProvider,它们都属于系统回调式 API。
共同特点是:
- 需要注册监听器
- 需要取消注册
- 可能需要发送初始值
- 适合包装成冷流
- 需要用
awaitClose释放资源
统一模式可以写成:
然后在 Repository 中继续保持冷流,不要提前 stateIn / shareIn:
最后在 ViewModel 中转换成 UI 所需的 StateFlow:
8. 推荐的分层规则
这个规则可以让每一层职责更稳定:
- 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 架构边界的设计。