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有两个选项——KEEPREPLACE。如果你用KEEP,即使你再次调用enqueue,之前的任务也不会被覆盖。我刚开始没注意,结果每次App启动都调一次enqueue,导致同一个任务被重复调度了好几次。嗯,后来改成REPLACE才解决。

注意:周期性任务的间隔不能小于15分钟。如果你传了10分钟,WorkManager会强制改成15分钟。别问我怎么知道的,看源码发现的。

延迟任务:让任务“睡一会儿”再执行

有些任务不需要立刻执行。比如用户刚注册完,你希望等5分钟后再发一条欢迎推送。这时候就用到了延迟任务。

延迟任务的核心是setInitialDelay

val delayedRequest = OneTimeWorkRequestBuilder<WelcomeWorker>()
    .setInitialDelay(5, TimeUnit.MINUTES)
    .build()

WorkManager.getInstance(context).enqueue(delayedRequest)

你可能会问:如果用户在这5分钟内关闭了App,任务还会执行吗?会的。WorkManager会把任务交给系统,系统会在指定时间后唤醒App来执行。这就是WorkManager比普通线程池强的地方——它不依赖App进程存活。

我曾经做过一个功能:用户下单后,如果30分钟内未支付,自动取消订单。用延迟任务就非常合适。但要注意,延迟任务不能保证精确到秒,它只是一个“最早执行时间”。系统可能会因为省电策略,稍微推迟几分钟。

任务观察:实时掌握任务状态

任务提交了,怎么知道它跑没跑完?WorkManager提供了LiveDataFlow两种观察方式。

我个人习惯用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需要返回多个结果,可以定义一个数据类,用Gson转成String再放进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