8、任务输入与输出:数据传递的艺术
WorkManager 的任务调度,说白了就是一条流水线。上游 Worker 干完活,得把结果传给下游。这个传递过程,就是我今天要聊的核心——输入数据、输出数据、结果传递。
我在项目中见过不少新手,直接把对象塞进 Data.Builder,结果运行时崩了。嗯,这里有个关键点:Data 只能存基本类型和字符串。你想想看,WorkManager 要跨进程、跨重启,怎么可能让你随便传一个复杂对象?
8.1 输入数据:Data.Builder 的正确用法
先看一个最简单的例子。我要给 Worker 传一个用户 ID 和任务类型:
val inputData = Data.Builder()
.putString("user_id", "12345")
.putInt("task_type", 1)
.build()
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
.setInputData(inputData)
.build()
在 Worker 里怎么取?
class MyWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val userId = inputData.getString("user_id") ?: return Result.failure()
val taskType = inputData.getInt("task_type", 0)
// 干活...
return Result.success()
}
}
我曾经犯过一个低级错误:用 putString 存了一个 JSON 字符串,长度超过 10KB。结果 WorkManager 直接抛异常了。Data 的大小限制是 10KB,超过这个值请考虑用 Room 或文件存储。
8.2 输出数据:从 Worker 返回结果
Worker 干完活,怎么把结果传出去?用 Result.success(outputData)。我个人习惯把输出数据封装成一个方法,保持代码整洁:
class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
return try {
val filePath = inputData.getString("file_path") ?: return Result.failure()
val uploadResult = uploadFile(filePath)
val outputData = Data.Builder()
.putString("upload_url", uploadResult.url)
.putLong("file_size", uploadResult.size)
.build()
Result.success(outputData)
} catch (e: Exception) {
Result.failure()
}
}
private fun uploadFile(path: String): UploadResult {
// 模拟上传
return UploadResult("https://cdn.example.com/file.jpg", 1024L)
}
}
Result.success() 和 Result.failure() 都可以携带 outputData。哪怕任务失败了,你也可以传一些错误信息给下游。
8.3 结果传递:链式任务的输入输出
这才是重头戏。WorkManager 支持任务链,一个 Worker 的输出,自动成为下一个 Worker 的输入。我画了一张图帮你理解:
代码实现也很直观:
val downloadWork = OneTimeWorkRequestBuilder<DownloadWorker>()
.setInputData(Data.Builder().putString("url", imageUrl).build())
.build()
val compressWork = OneTimeWorkRequestBuilder<CompressWorker>()
.build()
val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>()
.build()
WorkManager.getInstance(context)
.beginWith(downloadWork)
.then(compressWork)
.then(uploadWork)
.enqueue()
注意看,compressWork 和 uploadWork 没有显式设置 inputData。为什么?因为 WorkManager 会自动把上一个 Worker 的 outputData 传递过来。你只需要在 Worker 里用 inputData 读取就行。
我建议在 Worker 的 doWork() 方法开头,统一做一次 inputData 的解析。如果缺少必要字段,直接返回 Result.failure()。这样下游 Worker 就不会因为空数据而崩溃。
8.4 实战:图片处理流水线
拿一个真实场景举例。我在做一个社交 App 时,用户上传头像需要经过:下载原图 → 压缩 → 上传 CDN。三个任务串起来:
// DownloadWorker
class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val url = inputData.getString("image_url") ?: return Result.failure()
val localPath = downloadToLocal(url)
return Result.success(
Data.Builder().putString("local_path", localPath).build()
)
}
}
// CompressWorker
class CompressWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val localPath = inputData.getString("local_path") ?: return Result.failure()
val compressedPath = compressImage(localPath)
return Result.success(
Data.Builder().putString("compressed_path", compressedPath).build()
)
}
}
// UploadWorker
class UploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val compressedPath = inputData.getString("compressed_path") ?: return Result.failure()
val uploadUrl = uploadToCdn(compressedPath)
return Result.success(
Data.Builder().putString("cdn_url", uploadUrl).build()
)
}
}
这个模式的好处是:每个 Worker 职责单一,数据流清晰。你想想看,如果哪天要换压缩算法,只需要改 CompressWorker,其他两个完全不用动。
Data 的键值对是扁平的,不支持嵌套。如果你需要传递复杂结构,我建议用 JSON 序列化成字符串,但记得控制大小在 10KB 以内。超过这个量,请改用 Room 数据库或者文件路径传递。
8.5 总结
数据传递是 WorkManager 任务链的命脉。记住三点:
- 输入用 Data.Builder,只支持基本类型和字符串
- 输出用 Result.success(outputData),失败也能带数据
- 链式任务自动传递,上一个的输出就是下一个的输入
我个人习惯在项目里定义一个 DataKeys 常量类,把所有键名集中管理,避免拼写错误。比如:
object DataKeys {
const val USER_ID = "user_id"
const val IMAGE_URL = "image_url"
const val LOCAL_PATH = "local_path"
const val COMPRESSED_PATH = "compressed_path"
const val CDN_URL = "cdn_url"
}
这样写起来舒服,维护起来也省心。好了,数据传递这块就聊到这。记住,WorkManager 的数据传递虽然简单,但用好了能让你的任务链像瑞士手表一样精准。