13、WorkManager进阶:周期性任务、延迟任务、任务观察、输入输出、测试、实战:定时同步数据。
上一章我们把WorkManager的基本用法捋了一遍。说实话,那只是开胃菜。真正到了生产环境,你会发现需求远不止“跑一次任务”这么简单。比如:App需要每隔15分钟同步一次用户数据;或者用户关闭App后,还想在凌晨2点偷偷做一次日志上传;又或者,你需要知道任务到底跑没跑成功,失败了怎么重试。
这些场景,才是WorkManager真正发光的地方。今天我们就来啃这些硬骨头。
周期性任务:不只是“重复执行”那么简单
先说说周期性任务。很多人以为它就是“每隔X秒执行一次”,其实不是。WorkManager的周期性任务,最小间隔是15分钟。为什么?因为Google想省电。你想想看,如果每个App都每隔几秒唤醒一次系统,手机半天就没电了。
我在项目中遇到过这样一个需求:每隔30分钟拉取一次天气数据。当时我直接用PeriodicWorkRequest,结果发现任务执行的时间点并不精确。比如你设置15分钟,它可能在第16分钟、第18分钟才跑。这是因为WorkManager会合并多个App的唤醒请求,统一调度。
来看代码怎么写:
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"sync_weather",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
这里有个坑:ExistingPeriodicWorkPolicy有两个选项——KEEP和REPLACE。如果你用KEEP,即使你再次调用enqueue,之前的任务也不会被覆盖。我刚开始没注意,结果每次App启动都调一次enqueue,导致同一个任务被重复调度了好几次。嗯,后来改成REPLACE才解决。
延迟任务:让任务“睡一会儿”再执行
有些任务不需要立刻执行。比如用户刚注册完,你希望等5分钟后再发一条欢迎推送。这时候就用到了延迟任务。
延迟任务的核心是setInitialDelay:
val delayedRequest = OneTimeWorkRequestBuilder<WelcomeWorker>()
.setInitialDelay(5, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(context).enqueue(delayedRequest)
你可能会问:如果用户在这5分钟内关闭了App,任务还会执行吗?会的。WorkManager会把任务交给系统,系统会在指定时间后唤醒App来执行。这就是WorkManager比普通线程池强的地方——它不依赖App进程存活。
我曾经做过一个功能:用户下单后,如果30分钟内未支付,自动取消订单。用延迟任务就非常合适。但要注意,延迟任务不能保证精确到秒,它只是一个“最早执行时间”。系统可能会因为省电策略,稍微推迟几分钟。
任务观察:实时掌握任务状态
任务提交了,怎么知道它跑没跑完?WorkManager提供了LiveData和Flow两种观察方式。
我个人习惯用WorkInfo.getLiveData(),因为它和Android的生命周期天然契合:
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(workRequest.id)
.observe(this, Observer { workInfo ->
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
// 任务成功
val outputData = workInfo.outputData
val result = outputData.getString("result", "")
}
WorkInfo.State.FAILED -> {
// 任务失败
}
WorkInfo.State.RUNNING -> {
// 正在执行
}
else -> {}
}
}
})
这里有个细节:workInfo.outputData可以拿到Worker返回的数据。比如你的Worker上传完文件后,返回了服务器地址,观察者就能拿到这个地址。
如果你用Kotlin,推荐用flow方式:
WorkManager.getInstance(context)
.getWorkInfoByIdFlow(workRequest.id)
.collect { workInfo ->
// 处理状态变化
}
为什么推荐Flow?因为它在协程里用起来更自然,而且可以配合StateFlow做UI绑定。
输入输出:让Worker“有来有回”
Worker不是孤岛。它需要接收外部数据,也需要把结果传出去。WorkManager用Data对象来传递输入输出。
先看输入:
val inputData = Data.Builder()
.putString("url", "https://api.example.com/sync")
.putInt("timeout", 30)
.build()
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(inputData)
.build()
Worker里怎么取?
class SyncWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val url = inputData.getString("url")
val timeout = inputData.getInt("timeout", 10)
// 执行同步逻辑
return Result.success()
}
}
再看输出。Worker执行完后,可以返回结果给观察者:
override fun doWork(): Result {
val outputData = Data.Builder()
.putString("sync_result", "success")
.putLong("sync_time", System.currentTimeMillis())
.build()
return Result.success(outputData)
}
我在项目中遇到过一个问题:Data对象的大小限制是10KB。如果你传一个很大的JSON字符串,会直接崩溃。所以,大数据请用文件或数据库,别往Data里塞。
测试:别让Worker成为“黑盒”
很多开发者写完Worker就不管了,上线后才发现问题。其实WorkManager提供了很好的测试支持。
首先,添加测试依赖:
testImplementation "androidx.work:work-testing:2.9.0"
然后,用TestListenableWorkerBuilder来模拟Worker执行:
@Test
fun testSyncWorker() {
val context = ApplicationProvider.getApplicationContext<Context>()
val inputData = Data.Builder()
.putString("url", "https://test.com")
.build()
val worker = TestListenableWorkerBuilder<SyncWorker>(context)
.setInputData(inputData)
.build()
val result = worker.doWork()
assertThat(result, `is`(Result.success()))
}
如果你想测试任务调度,可以用TestDriver:
@Test
fun testPeriodicWork() {
val context = ApplicationProvider.getApplicationContext<Context>()
val testDriver = TestDriver()
val workManager = WorkManagerTestInitHelper.getTestWorkManager(context, testDriver)
// 提交任务
val request = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES).build()
workManager.enqueue(request).result.get()
// 模拟时间流逝
testDriver.setPeriodDelayMet(1, TimeUnit.MINUTES)
// 验证任务状态
val workInfo = workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
}
我曾经因为没写测试,导致一个Worker在低端手机上频繁崩溃。原因是Worker里用了BitmapFactory加载大图,而测试环境没暴露这个问题。后来加了测试,才把这类问题扼杀在摇篮里。
实战:定时同步数据
好了,理论说完了,我们来做一个完整的实战:定时同步用户数据到服务器。
需求很简单:每隔30分钟,把本地数据库里的用户操作记录上传到服务器。如果网络失败,重试3次,每次间隔10秒。
先定义Worker:
class DataSyncWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
return try {
// 1. 从本地数据库读取待同步数据
val pendingData = LocalDatabase.getPendingSyncData()
if (pendingData.isEmpty()) {
return Result.success()
}
// 2. 上传到服务器
val apiService = RetrofitClient.create(ApiService::class.java)
val response = apiService.syncData(pendingData).execute()
if (response.isSuccessful) {
// 3. 标记已同步
LocalDatabase.markAsSynced(pendingData)
Result.success()
} else {
Result.retry()
}
} catch (e: Exception) {
// 网络异常,重试
Result.retry()
}
}
}
然后配置重试策略:
val syncRequest = PeriodicWorkRequestBuilder<DataSyncWorker>(
30, TimeUnit.MINUTES
).setBackoffCriteria(
BackoffPolicy.LINEAR,
10, TimeUnit.SECONDS
).build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"data_sync",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
这里我用了BackoffPolicy.LINEAR,意思是每次重试间隔线性增加。第一次10秒,第二次20秒,第三次30秒。如果你用EXPONENTIAL,间隔会指数增长,适合服务器压力大的场景。
最后,别忘了在AndroidManifest.xml里声明权限(如果需要网络):
<uses-permission android:name="android.permission.INTERNET" />
- 周期性任务最小间隔15分钟,别指望精确执行
- 延迟任务适合“过一会儿再干”的场景
- 用LiveData或Flow观察任务状态,别轮询
- 输入输出用Data对象,注意10KB限制
- 测试Worker用TestListenableWorkerBuilder,别偷懒
说实话,WorkManager最让我欣赏的地方,就是它把“什么时候执行”这件事完全交给了系统。你只需要关心“做什么”,剩下的调度、省电、重试,WorkManager都帮你搞定了。这种设计思路,值得我们在写代码时借鉴。
好了,今天的进阶内容就到这里。代码都给你了,赶紧去试试吧。
公众号:蓝海资料掘金营,微信deep3321