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 的输入。我画了一张图帮你理解:

WorkManager 任务链数据传递流程 Worker A 下载图片 输出: file_path outputData Worker B 压缩图片 输出: compressed_path outputData Worker C 上传图片 输出: upload_url 数据传递规则: 1. Worker A 的 outputData 自动成为 Worker B 的 inputData 2. 每个 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 的数据传递虽然简单,但用好了能让你的任务链像瑞士手表一样精准。

公众号:蓝海资料掘金营,微信deep3321