26、Content Provider 与 WorkManager 整合:后台同步、定期更新、数据一致性
说实话,Content Provider 和 WorkManager 这对组合,在真实项目中用得比很多人想象中要多。我见过不少团队,数据同步逻辑写得乱七八糟,要么是 Service 满天飞,要么是定时器到处挂。最后呢?电量耗得快,数据还对不上。
我个人习惯是:凡是需要后台定期同步、又要保证数据一致性的场景,直接用 WorkManager 调度 Content Provider 的操作。这套组合拳打好了,省心又省电。
为什么是 WorkManager?
你想想看,Android 的后台任务管理越来越严。从 Oreo 的后台限制,到 Pie 的省电模式,再到高版本的自启动限制。传统的 Service 方案,说白了就是在跟系统对着干。
WorkManager 的好处很明显:
- 兼容性好:API 14+ 都能用,系统会自动选择最佳执行方式
- 生命周期感知:不会因为应用被杀就丢失任务
- 约束条件灵活:可以指定网络状态、电量、空闲状态等
- 链式调用:多个任务可以串起来执行
我在项目中遇到过最典型的场景:用户修改了本地数据,需要同步到服务器,同时还要更新 Content Provider 里的缓存。用 WorkManager 一把梭,比用 AlarmManager + Service 优雅太多了。
核心架构设计
先看整体流程,我画了张图帮你理解:
实战:构建一个定期同步的 Worker
嗯,这里要注意。Worker 里操作 Content Provider,不能直接拿 Activity 的 Context 去搞。要用 applicationContext,因为 Worker 可能在后台运行,跟 UI 没关系。
先看一个基础的 SyncWorker:
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// 用 applicationContext 操作 Content Provider
val uri = Uri.parse("content://com.example.app/sync_data")
val values = ContentValues().apply {
put("last_sync_time", System.currentTimeMillis())
put("sync_status", "in_progress")
}
// 更新同步状态
applicationContext.contentResolver.update(
uri,
values,
null,
null
)
// 执行实际的数据同步逻辑
performSync()
// 同步完成,更新状态
values.put("sync_status", "completed")
applicationContext.contentResolver.update(
uri,
values,
null,
null
)
Result.success()
} catch (e: Exception) {
Log.e("SyncWorker", "同步失败", e)
Result.retry()
}
}
private suspend fun performSync() {
// 这里写你的同步逻辑
// 比如从服务器拉数据,然后写入 Content Provider
}
}
定期同步的调度策略
定期同步用 PeriodicWorkRequest。但有个坑:最小间隔是 15 分钟,这是系统强制的。你设 5 分钟也没用,系统会给你拉到 15 分钟。
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
30, // 间隔
TimeUnit.MINUTES,
15, // 灵活期(flex interval)
TimeUnit.MINUTES
).apply {
// 设置约束条件
setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // 需要网络
.setRequiresBatteryNotLow(true) // 电量不能太低
.build()
)
// 重试策略
setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30,
TimeUnit.SECONDS
)
}.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"sync_work",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
数据一致性保障
说白了,后台同步最怕什么?数据冲突。用户本地改了,后台也在改,到底听谁的?
我常用的策略是版本号 + 时间戳双保险:
// Content Provider 的 insert 方法
override fun insert(uri: Uri, values: ContentValues?): Uri? {
values?.let {
// 自动维护版本号
it.put("version", System.currentTimeMillis())
it.put("sync_status", "pending")
}
return super.insert(uri, values)
}
// Worker 中的冲突处理
private fun resolveConflict(localVersion: Long, remoteVersion: Long): Boolean {
// 谁版本新听谁的
return localVersion > remoteVersion
}
还有个细节:ContentObserver。Worker 更新完数据后,记得通知观察者。这样 UI 层能及时刷新:
// 在 Content Provider 的 update 方法末尾
context.contentResolver.notifyChange(uri, null)
// 在 Activity 中注册观察者
contentResolver.registerContentObserver(
uri,
true,
object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
// 刷新 UI
loadData()
}
}
)
错误处理与重试策略
我见过最糟糕的写法:同步失败就静默吞掉异常。结果用户数据丢了都不知道。
正确的做法是:
| 错误类型 | 处理方式 | 重试策略 |
|---|---|---|
| 网络超时 | Result.retry() | 指数退避,最多 3 次 |
| 数据格式错误 | Result.failure() | 不重试,记录日志 |
| 权限不足 | Result.failure() | 不重试,提示用户 |
| 服务器 500 | Result.retry() | 固定间隔 1 分钟,最多 5 次 |
override suspend fun doWork(): Result {
return when (val result = performSync()) {
is SyncResult.Success -> Result.success()
is SyncResult.RetryableError -> {
if (runAttemptCount < 3) {
Result.retry()
} else {
// 记录到本地,下次启动再试
saveFailedSync(result.error)
Result.failure()
}
}
is SyncResult.FatalError -> {
// 记录错误,不重试
logError(result.error)
Result.failure()
}
}
}
- WorkManager 负责调度,Content Provider 负责数据
- Worker 里用 applicationContext 操作 Provider
- 定期同步用 PeriodicWorkRequest,注意最小间隔限制
- 版本号 + 时间戳解决数据冲突
- ContentObserver 实现 UI 自动刷新
- 错误处理要区分可重试和不可重试
最后说一句:这套方案我用了两年多,线上跑得很稳。你只要把约束条件设好,重试策略配好,剩下的交给 WorkManager 就行。系统会帮你处理好省电、兼容性这些破事。