一、什么是离线优先
离线优先不是“没有网络时给个错误页”,而是应用的核心流程尽量先依赖本地数据,网络只负责同步。
用户打开页面时,优先读取本地数据库;网络请求成功后再更新本地数据,并通过响应式数据流刷新界面。
这种设计适合:
- 笔记、待办、表单采集。
- IM 消息列表。
- 工单、巡检、仓储。
- 弱网环境下的企业应用。
不适合简单套用的场景:
- 强实时交易。
- 必须服务端确认后才能展示的数据。
- 冲突代价极高的写操作。
二、推荐分层
常见分层:
1 | UI -> ViewModel -> Repository -> Local DB |
职责划分:
- UI:只展示状态和提交用户事件。
- ViewModel:组合页面状态。
- Repository:决定读本地还是读远端。
- Local DB:作为页面的主要数据源。
- Remote API:作为同步来源或提交目标。
三、读流程
读流程建议固定为:
- 页面订阅本地数据库。
- Repository 触发远端刷新。
- 刷新成功后写入数据库。
- UI 因数据库变化自动刷新。
Kotlin 示例:
1 | class TaskRepository( |
这样页面不会直接依赖网络请求结果,弱网时也能看到旧数据。
四、写流程
写操作要先考虑用户体验。常见做法是本地先写入,再后台同步:
1 | user edit -> local pending record -> UI update -> background sync |
实体中可以增加同步字段:
1 | data class TaskEntity( |
用户修改后,先写本地:
1 | suspend fun updateTitle(id: String, title: String) { |
五、使用 WorkManager 同步
WorkManager 适合处理可延迟、需要保证执行的后台同步任务。
示例:
1 | val request = OneTimeWorkRequestBuilder<SyncWorker>() |
同步任务里按顺序处理 pending 数据:
1 | class SyncWorker( |
六、冲突处理
冲突来自多个端同时修改同一条数据。
常见策略:
- 客户端覆盖服务端:实现简单,但可能丢数据。
- 服务端覆盖客户端:安全保守,但用户修改可能消失。
- 按字段合并:适合结构化表单。
- 人工确认:适合重要数据。
- 版本号控制:服务端拒绝旧版本提交。
实体可以增加版本字段:
1 | data class TaskEntity( |
提交时带上版本号,服务端发现版本过旧就返回冲突,客户端再进入合并流程。
七、错误和状态展示
离线优先应用不要只显示一个 loading。页面状态可以拆成:
data:当前可展示的数据。isRefreshing:是否正在刷新。pendingCount:待同步数量。lastSyncTime:上次成功同步时间。errorMessage:最近一次错误。
用户更关心“现在能不能继续操作”,而不是网络请求是否绝对成功。
八、实践清单
- 页面主要观察本地数据库。
- 网络刷新只写库,不直接驱动页面。
- 写操作记录 pending 状态。
- WorkManager 负责后台补偿同步。
- 所有同步任务可重入、可重试。
- 删除操作要有 tombstone 或 pending delete。
- 重要业务必须定义冲突策略。
- 本文链接: https://blog.hansong.icu/2026/06/21/Android_Offline_First_App/
- 版权声明: 本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。