WorkManager 核心三件套:Worker、WorkRequest 与 WorkManager 实例

讲依赖注入和 Mock 测试之前,得先把地基打牢。WorkManager 最核心的三样东西,说白了就是 Worker、WorkRequest 和 WorkManager 实例。我在项目中见过不少新手一上来就写代码,结果任务跑不起来,回头一看,这三者的关系根本没理清。嗯,今天我们就把它彻底掰开揉碎。

一句话总结三者的关系:

  • Worker:干活的人,定义“做什么”
  • WorkRequest:任务单,定义“什么时候干、有什么条件”
  • WorkManager:调度中心,负责把任务单分发给干活的人,并管理整个生命周期
WorkManager 核心概念关系图 Worker 定义任务执行逻辑 doWork() / Result WorkRequest 配置任务约束 OneTime / Periodic WorkManager 调度与生命周期管理 enqueue() / getInstance() 指定 提交 调度并执行 Worker 核心方法 · doWork(): 执行耗时逻辑 · onStopped(): 取消时回调 · getForegroundInfo(): 前台通知 WorkRequest 配置 · Constraints: 网络/充电条件 · BackoffPolicy: 重试策略 · Tag / Id / InputData WorkManager 实例 · getInstance(): 单例获取 · WorkManagerBuilder: 自定义 · WorkContinuation: 链式组合 💡 核心工作流(记住这个顺序) 1. 继承 Worker ➔ 实现 doWork() 2. 构建 WorkRequest ➔ 绑定 Worker.class + 配置约束 3. 获取 WorkManager 实例 ➔ 调用 enqueue(workRequest)

1. Worker——真正干活的家伙

Worker 是一个抽象类,你需要继承它并实现 doWork() 方法。我最早接触时以为它就是个普通的 Runnable,后来才发现它在后台运行在 WorkManager 管理的线程池里,生命周期有严格保障。

class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    
    override fun doWork(): Result {
        // 这里是你的后台逻辑
        val imageUri = inputData.getString("image_uri") ?: return Result.failure()
        return try {
            uploadImage(imageUri)
            Result.success()
        } catch (e: Exception) {
            // 重试策略会自动处理
            Result.retry()
        }
    }
}

个人经验:Worker 的构造器有两个参数——Context 和 WorkerParameters。我在项目中踩过一个坑:直接在构造器里做耗时操作!记住,doWork() 才是真正的工作入口,Worker 实例本身是初始化用的,别在里面搞网络请求。

Worker 有三种返回结果,记住它们就好:

返回类型 含义 行为
Result.success() 任务执行成功 WorkManager 标记完成,不再重试
Result.failure() 任务永久失败 WorkManager 放弃该任务,不会再重试
Result.retry() 需要重试 根据 BackoffPolicy 配置的延迟时间重新调度

嗯,这里要注意一个常见的坑:Result.retry() 不是无限重试的,它有一个默认的最大次数限制。我曾经有个任务一直 retry,结果 Debug 模式下看起来正常,上线后用户反馈日志里全是失败——因为默认重试次数用完了,任务直接被标记为失败。

2. WorkRequest——任务配置单

Worker 定义了做什么,而 WorkRequest 定义了 什么时候做、有什么条件。它有点像 Android 的 Intent——你告诉系统你想干啥,但具体执行时机可能由系统决定。

WorkRequest 有两种风味:

// 一次性任务:执行一次,适合单次同步操作
val oneTimeRequest = OneTimeWorkRequestBuilder<UploadWorker>()
    .setInputData(workDataOf("image_uri" to "file:///..."))
    .addTag("upload")
    .build()

// 周期性任务:循环执行,适合定时检查
val periodicRequest = PeriodicWorkRequestBuilder<CheckWorker>(
        15, TimeUnit.MINUTES  // 最小间隔是 15 分钟
    )
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    )
    .build()

我曾经踩过的坑:刚接触 PeriodicWorkRequest 时,我天真地以为可以设置 5 秒的间隔。结果 Google 为了省电,强制最短周期为 15 分钟。你想想看,如果每隔几秒就跑一次后台任务,用户手机不得分分钟没电?所以官方设计是有道理的。

WorkRequest 最常用的配置项我整理了一下:

配置项 对应方法 说明
输入数据 setInputData() 通过键值对传递参数给 Worker
执行约束 setConstraints() 如网络状态、充电状态、设备空闲等
重试策略 setBackoffCriteria() 线性或指数退避,默认 30 秒后重试
标签 addTag() 用于按标签取消或查询任务组
唯一标识 setId() 不设置会自动生成 UUID

3. WorkManager 实例——调度中心

前面两个准备好了,总得有人来调度。WorkManager 是全局单例,通过 WorkManager.getInstance() 获取。但我得强调一点:千万别在 Application.onCreate() 里手动初始化,除非你需要自定义配置。

// 最常用的方式——自动初始化(默认配置)
WorkManager.getInstance(context)

// 如果你需要自定义配置
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        // 自定义配置
        val config = Configuration.Builder()
            .setMinimumLoggingLevel(android.util.Log.INFO)
            .setWorkerFactory(MyWorkerFactory())  // 依赖注入的关键!
            .build()
        WorkManager.initialize(this, config)
    }
}

为什么说 WorkManager.getInstance() 不是简单的事?

默认情况下,WorkManager 通过 ContentProvider 自动初始化。但一旦你牵扯到依赖注入(比如 Dagger/Hilt 或 Koin),你就需要手动设置 WorkerFactory。原因很简单:Worker 是通过反射创建的,系统默认的 WorkerFactory 不知道你的依赖怎么注入。嗯,这正是我们后面几章要重点解决的。

提交任务也很简单:

val workManager = WorkManager.getInstance(context)
workManager.enqueue(oneTimeRequest)

// 如果你有多个任务顺序执行
WorkManager.getInstance(context)
    .beginWith(oneTimeRequest)
    .then(nextRequest)
    .enqueue()

这里有个细节:enqueue() 是异步的,它返回一个 Operation 对象,你可以监听完成状态。但说实话,大部分场景下你不需要关心它什么时候完成,交给 WorkManager 去管理就好——它会处理进程重启、设备重启等极端情况。

核心流程速览

把这三个东西串起来,你得到的就是整个 WorkManager 的工作流:

  1. 定义 Worker:继承 Worker,在 doWork() 里写你的业务逻辑
  2. 构建 WorkRequest:用 Builder 创建任务单,配置约束和输入数据
  3. 获取 WorkManager 实例:调用 WorkManager.getInstance()
  4. 入队执行:workManager.enqueue(workRequest)

一个小技巧:如果你的项目从 RxJava 或 LiveData 迁移到 WorkManager,你会发现它天然支持协程——用 CoroutineWorker 替代普通的 Worker,doWork() 可以直接写成挂起函数,代码更简洁。我现在的项目基本都用 CoroutineWorker 了。