本文基于对 Learn-Kotlin-Coroutines 的工程化重构,记录从「请求式架构」走向「响应式单一数据源(SSOT)」的完整思路与实现方案。
引言:被网络绑架的 UI
过去很多 Android 项目的数据流都是这样的:
请求网络 → 拿到数据 → 更新 UI
这种模式在网络稳定时看起来没有问题。但一旦网络变慢、接口超时、页面频繁切换,问题就会迅速暴露:
- 页面白屏等待
- 数据状态不一致
- 本地缓存形同虚设
根本原因在于:UI 的命运被网络状态决定了。
现代 Android 架构正在转向另一种思路------UI 不直接依赖网络,而是永远依赖本地数据库:
UI ← Database ← Network
数据库(Room)成为 Single Source of Truth(SSOT,单一真实数据源) ,网络只负责在后台更新它。这也是 Offline-first、响应式编程、Jetpack 官方架构背后共同的核心思想。
一、什么是真正的离线优先
很多人以为"有缓存 = 离线优先",其实不是。
真正的离线优先强调的不是「有没有缓存」,而是**「UI 是否依赖网络返回」**。即使网络完全不可用,UI 依然能正常展示和响应。
推荐的数据流架构如下:
scss
UI
↓ 观察状态流
ViewModel
↓ 请求数据
Repository
↓ 读写
Room (Database)
↑ 后台同步
Network
各层职责清晰:
| 模块 | 职责 |
|---|---|
| UI | 只负责观察并渲染状态 |
| ViewModel | 将 Repository 数据流转为 UI 状态 |
| Repository | 数据协调中心,管理同步策略 |
| Database | 唯一真实数据源(SSOT) |
| Network | 数据库的后台更新器 |
二、重构路径:从旧架构到离线优先
理解架构思路之前,先看清楚旧代码的问题所在。
旧架构(网络直接驱动 UI)

问题: 网络失败 → UI 直接进入错误状态,本地数据库里即使有缓存也无法展示。
新架构(数据库驱动 UI)UI 永远观察数据库,网络在后台静默同步

效果: 无论网络状态如何,UI 始终展示数据库中的最新数据;网络同步成功后,Room 的 Flow 自动触发 UI 刷新。
三、三种核心实现方案
实际工程中,离线优先的实现会随着项目规模演进出三种方案,分别代表数据层架构的三个阶段。
方案一:顺序流模式(Sequential Flow)
使用标准 flow {} 按顺序组织:先读缓存、后同步网络、最后监听数据库。

核心特点: 数据库最终驱动 UI,但第三步的 emitAll() 必须等待网络同步完成后才会启动。这意味着在网络返回之前,数据库的变化不会实时推送给 UI------串行时序是它的致命缺陷。
| 适用场景 | 原型验证、Demo、协程入门项目 |
| 团队规模 | 个人或小团队 |
| 优点 | 逻辑直观,符合顺序思维,调试成本低 |
| 缺点 | 数据库监听被网络请求串行阻塞,弱网时用户依然需要等待 |
方案二:并发同步模式(ChannelFlow)✦ 推荐
这是本项目采用的核心方案。channelFlow + launch 让数据库监听与网络同步真正并行运行。

channelFlow + launch 的真正价值不是「更快」,而是解耦:
| 模块 | 是否互相阻塞 |
|---|---|
| 数据库监听 | 不会 |
| 网络同步 | 不会 |
| UI 更新 | 不会 |
网络超时、请求失败、接口卡顿都不会影响 UI,缓存数据始终可以秒开。这也是 Google 官方的 Paging3、RemoteMediator、Store5 本质上都在做的事。
| 适用场景 | 中型项目、高频刷新页面、Room + Flow + Compose |
| 团队规模 | 中小团队 |
| 优点 | 真正响应式解耦,真正离线优先,符合现代 Android 架构 |
| 缺点 | 需要理解 channelFlow、多协程生产 Flow 的并发模型 |
方案三:架构抽象模式(NetworkBoundResource)
当项目规模继续扩大,问题不再是"怎么写 Repository",而是**"如何统一整个项目的数据同步策略"**。NetworkBoundResource(NBR)将同步流程抽象为模板函数:

Repository 的调用方只需关心业务语义,完全不用关心同步细节:

| 适用场景 | 大型项目、需要统一数据层规范 |
| 团队规模 | 中大型团队 |
| 优点 | 极致复用,统一错误处理、缓存策略、加载状态,便于团队协作 |
| 缺点 | 抽象成本高,新人难以理解数据流走向;特殊业务(增量同步、多源聚合)灵活性下降 |
四、三种方案横向对比
| 方案 | 数据库监听时机 | 网络与监听关系 | 适用规模 | 学习成本 |
|---|---|---|---|---|
| Sequential Flow | 网络完成后 | 串行阻塞 | 小型 | 低 |
| ChannelFlow | 立即开始 | 并行解耦 | 中型 | 中 |
| NetworkBoundResource | 立即开始 | 并行解耦 | 大型 | 高 |
三者并非替代关系,而是演进关系。ChannelFlow 方案处于工程复杂度和架构收益的最佳平衡点:比传统 Flow 更现代,比 NBR 更轻量,足以解决绝大多数真实业务问题。
五、统一数据源
1. Single Source of Truth(SSOT)落地
Room 数据库成为 UI 的唯一真实数据源,彻底解决了多数据源同步时的状态不一致问题。
2. 响应式 UI(Reactive UI)
ViewModel 使用 stateIn() 将 Repository 的 Flow 转换为 StateFlow,UI 层不再主动发起刷新,而是自动响应数据变化:
ini
val usersState = repository.getUsers()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Resource.Loading
)
3. 分层异常处理
异常收敛在 Data Layer,ViewModel 和 UI 只感知 Resource.Success / Error / Loading 状态,不接触 try-catch。
4. Repository 成为真正的数据协调中心
Repository 不再只是网络接口的透传层,而是统一负责:本地缓存读写、网络同步触发、数据合并、错误降级、数据流协调。
总结
现代 Android 架构最本质的变化不仅仅是引入了 Kotlin、协程或 Compose,而是数据流思想的转变:
| 时代 | 模式 | 特点 |
|---|---|---|
| 过去 | 网络驱动 UI | UI 等待网络,弱网即白屏 |
| 现在 | 数据库驱动 UI | UI 响应数据流,网络只是搬运工 |
Offline-first 正是这种思想的最终体现。