banner
NEWS LETTER

Android 离线优先应用设计:缓存、同步、冲突和 WorkManager

Scroll down

一、什么是离线优先

离线优先不是“没有网络时给个错误页”,而是应用的核心流程尽量先依赖本地数据,网络只负责同步。

用户打开页面时,优先读取本地数据库;网络请求成功后再更新本地数据,并通过响应式数据流刷新界面。

这种设计适合:

  • 笔记、待办、表单采集。
  • IM 消息列表。
  • 工单、巡检、仓储。
  • 弱网环境下的企业应用。

不适合简单套用的场景:

  • 强实时交易。
  • 必须服务端确认后才能展示的数据。
  • 冲突代价极高的写操作。

二、推荐分层

常见分层:

1
2
3
UI -> ViewModel -> Repository -> Local DB
|
-> Remote API

职责划分:

  • UI:只展示状态和提交用户事件。
  • ViewModel:组合页面状态。
  • Repository:决定读本地还是读远端。
  • Local DB:作为页面的主要数据源。
  • Remote API:作为同步来源或提交目标。

三、读流程

读流程建议固定为:

  1. 页面订阅本地数据库。
  2. Repository 触发远端刷新。
  3. 刷新成功后写入数据库。
  4. UI 因数据库变化自动刷新。

Kotlin 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class TaskRepository(
private val dao: TaskDao,
private val api: TaskApi
) {
fun observeTasks(): Flow<List<TaskEntity>> {
return dao.observeAll()
}

suspend fun refresh() {
val remoteTasks = api.getTasks()
dao.upsertAll(remoteTasks.map { it.toEntity() })
}
}

这样页面不会直接依赖网络请求结果,弱网时也能看到旧数据。

四、写流程

写操作要先考虑用户体验。常见做法是本地先写入,再后台同步:

1
user edit -> local pending record -> UI update -> background sync

实体中可以增加同步字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data class TaskEntity(
val id: String,
val title: String,
val done: Boolean,
val syncState: SyncState,
val updatedAt: Long
)

enum class SyncState {
SYNCED,
PENDING_CREATE,
PENDING_UPDATE,
PENDING_DELETE,
FAILED
}

用户修改后,先写本地:

1
2
3
4
5
6
7
8
9
suspend fun updateTitle(id: String, title: String) {
dao.updateTitle(
id = id,
title = title,
syncState = SyncState.PENDING_UPDATE,
updatedAt = System.currentTimeMillis()
)
syncScheduler.enqueue()
}

五、使用 WorkManager 同步

WorkManager 适合处理可延迟、需要保证执行的后台同步任务。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30,
TimeUnit.SECONDS
)
.build()

WorkManager.getInstance(context)
.enqueueUniqueWork(
"task-sync",
ExistingWorkPolicy.KEEP,
request
)

同步任务里按顺序处理 pending 数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
repository.syncPendingChanges()
Result.success()
} catch (e: IOException) {
Result.retry()
} catch (e: Exception) {
Result.failure()
}
}
}

六、冲突处理

冲突来自多个端同时修改同一条数据。

常见策略:

  • 客户端覆盖服务端:实现简单,但可能丢数据。
  • 服务端覆盖客户端:安全保守,但用户修改可能消失。
  • 按字段合并:适合结构化表单。
  • 人工确认:适合重要数据。
  • 版本号控制:服务端拒绝旧版本提交。

实体可以增加版本字段:

1
2
3
4
5
6
data class TaskEntity(
val id: String,
val title: String,
val version: Long,
val updatedAt: Long
)

提交时带上版本号,服务端发现版本过旧就返回冲突,客户端再进入合并流程。

七、错误和状态展示

离线优先应用不要只显示一个 loading。页面状态可以拆成:

  • data:当前可展示的数据。
  • isRefreshing:是否正在刷新。
  • pendingCount:待同步数量。
  • lastSyncTime:上次成功同步时间。
  • errorMessage:最近一次错误。

用户更关心“现在能不能继续操作”,而不是网络请求是否绝对成功。

八、实践清单

  • 页面主要观察本地数据库。
  • 网络刷新只写库,不直接驱动页面。
  • 写操作记录 pending 状态。
  • WorkManager 负责后台补偿同步。
  • 所有同步任务可重入、可重试。
  • 删除操作要有 tombstone 或 pending delete。
  • 重要业务必须定义冲突策略。
其他文章
目录导航 置顶
  1. 1. 一、什么是离线优先
  2. 2. 二、推荐分层
  3. 3. 三、读流程
  4. 4. 四、写流程
  5. 5. 五、使用 WorkManager 同步
  6. 6. 六、冲突处理
  7. 7. 七、错误和状态展示
  8. 8. 八、实践清单
请输入关键词进行搜索