Android 离线优先架构实践:网络只是本地数据库的同步触发器

668 阅读6分钟

本文基于对 Learn-Kotlin-Coroutines 的工程化重构,记录从「请求式架构」走向「响应式单一数据源(SSOT)」的完整思路与实现方案。


引言:被网络绑架的 UI

过去很多 Android 项目的数据流都是这样的:

请求网络 → 拿到数据 → 更新 UI

这种模式在网络稳定时看起来没有问题。但一旦网络变慢、接口超时、页面频繁切换,问题就会迅速暴露:

  • 页面白屏等待
  • 数据状态不一致
  • 本地缓存形同虚设

根本原因在于:UI 的命运被网络状态决定了

现代 Android 架构正在转向另一种思路——UI 不直接依赖网络,而是永远依赖本地数据库:

UI ← Database ← Network

数据库(Room)成为 Single Source of Truth(SSOT,单一真实数据源) ,网络只负责在后台更新它。这也是 Offline-first、响应式编程、Jetpack 官方架构背后共同的核心思想。


一、什么是真正的离线优先

很多人以为"有缓存 = 离线优先",其实不是。

真正的离线优先强调的不是「有没有缓存」,而是**「UI 是否依赖网络返回」**。即使网络完全不可用,UI 依然能正常展示和响应。

推荐的数据流架构如下:

UI
 ↓  观察状态流
ViewModel
 ↓  请求数据
Repository
 ↓  读写
Room (Database)
 ↑  后台同步
Network

各层职责清晰:

模块职责
UI只负责观察并渲染状态
ViewModel将 Repository 数据流转为 UI 状态
Repository数据协调中心,管理同步策略
Database唯一真实数据源(SSOT)
Network数据库的后台更新器

二、重构路径:从旧架构到离线优先

理解架构思路之前,先看清楚旧代码的问题所在。

旧架构(网络直接驱动 UI)

image.png

问题: 网络失败 → UI 直接进入错误状态,本地数据库里即使有缓存也无法展示。

新架构(数据库驱动 UI)UI 永远观察数据库,网络在后台静默同步

image.png

效果: 无论网络状态如何,UI 始终展示数据库中的最新数据;网络同步成功后,Room 的 Flow 自动触发 UI 刷新。


三、三种核心实现方案

实际工程中,离线优先的实现会随着项目规模演进出三种方案,分别代表数据层架构的三个阶段。


方案一:顺序流模式(Sequential Flow)

使用标准 flow {} 按顺序组织:先读缓存、后同步网络、最后监听数据库。

image.png

核心特点: 数据库最终驱动 UI,但第三步的 emitAll() 必须等待网络同步完成后才会启动。这意味着在网络返回之前,数据库的变化不会实时推送给 UI——串行时序是它的致命缺陷。

适用场景原型验证、Demo、协程入门项目
团队规模个人或小团队
优点逻辑直观,符合顺序思维,调试成本低
缺点数据库监听被网络请求串行阻塞,弱网时用户依然需要等待

方案二:并发同步模式(ChannelFlow)✦ 推荐

这是本项目采用的核心方案。channelFlow + launch 让数据库监听与网络同步真正并行运行。

image.png

channelFlow + launch 的真正价值不是「更快」,而是解耦

模块是否互相阻塞
数据库监听不会
网络同步不会
UI 更新不会

网络超时、请求失败、接口卡顿都不会影响 UI,缓存数据始终可以秒开。这也是 Google 官方的 Paging3、RemoteMediator、Store5 本质上都在做的事。

适用场景中型项目、高频刷新页面、Room + Flow + Compose
团队规模中小团队
优点真正响应式解耦,真正离线优先,符合现代 Android 架构
缺点需要理解 channelFlow、多协程生产 Flow 的并发模型

方案三:架构抽象模式(NetworkBoundResource)

当项目规模继续扩大,问题不再是"怎么写 Repository",而是**"如何统一整个项目的数据同步策略"**。NetworkBoundResource(NBR)将同步流程抽象为模板函数:

image.png

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

image.png

适用场景大型项目、需要统一数据层规范
团队规模中大型团队
优点极致复用,统一错误处理、缓存策略、加载状态,便于团队协作
缺点抽象成本高,新人难以理解数据流走向;特殊业务(增量同步、多源聚合)灵活性下降

四、三种方案横向对比

方案数据库监听时机网络与监听关系适用规模学习成本
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 层不再主动发起刷新,而是自动响应数据变化:

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,而是数据流思想的转变

时代模式特点
过去网络驱动 UIUI 等待网络,弱网即白屏
现在数据库驱动 UIUI 响应数据流,网络只是搬运工

Offline-first 正是这种思想的最终体现。

Github Clean-Refactor