使用 WorkManager 进行后台工作

1. 开始之前

本 Codelab 涵盖了 WorkManager,这是一个向后兼容、灵活简单的库,用于可推迟的后台工作。WorkManager 是 Android 上推荐的可推迟工作的任务调度程序,保证执行。

前提条件

您将学习的内容

您将完成的任务

  • 修改起始应用以使用 WorkManager。
  • 实现用于模糊图片的 WorkRequest。
  • 通过链式连接工作来实现一系列串行工作。
  • 将数据传入和传出正在安排的工作。

所需条件

  • 最新稳定版 Android Studio
  • 互联网连接

2. 应用概览

如今的智能手机拍照功能几乎太好用了,以至于摄影师拍出模糊的神秘事物的时代已经一去不复返了。

在本 Codelab 中,你将使用 Blur-O-Matic,这是一款用于模糊照片并将结果保存到文件的应用。那究竟是 尼斯湖水怪还是玩具潜艇?使用 Blur-O-Matic,没人会知道!

屏幕上有单选按钮,你可以选择图片的模糊程度。点击开始按钮即可模糊图片并保存结果。

目前,应用尚未应用任何模糊效果,也未保存最终图片。

本 Codelab 重点介绍如何向应用添加 WorkManager,创建 Worker 以清理模糊图片过程中创建的临时文件、模糊图片并保存最终图片副本,点击查看文件按钮即可查看。你还将学习如何监控后台工作的状态并相应地更新应用的 UI。

3. 探索 Blur-O-Matic 起始应用

获取起始代码

要开始使用,请下载起始代码

或者,你可以克隆 GitHub 仓库获取代码

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git
$ cd basic-android-kotlin-compose-training-workmanager
$ git checkout starter

你可以在这个 GitHub 仓库中浏览 Blur-o-matic 应用的代码。

运行起始代码

要熟悉起始代码,请完成以下步骤

  1. 在 Android Studio 中打开包含起始代码的项目。
  2. 在 Android 设备或模拟器上运行应用。

2bdb6fdc2567e96.png

屏幕上有单选按钮,可让你选择图片的模糊量。当你点击开始按钮时,应用会模糊图片并保存。

目前,当你点击开始按钮时,应用不会应用任何模糊量。

起始代码演练

在此任务中,你将熟悉项目的结构。以下列表提供项目中的重要文件和文件夹的演练。

  • WorkerUtils:用于显示 Notifications 的便捷方法和将位图保存到文件的代码,你稍后会用到它们。
  • BlurViewModel:此视图模型存储应用的状态并与存储库交互。
  • WorkManagerBluromaticRepository:你使用 WorkManager 启动后台工作的类。
  • Constants:一个包含本 Codelab 中使用的常量值的静态类。
  • BluromaticScreen:包含用于 UI 的可组合函数,并与 BlurViewModel 交互。可组合函数显示图片并包含用于选择所需模糊级别的单选按钮。

4. 什么是 WorkManager?

WorkManager 是 Android Jetpack 的一部分,也是一个用于需要机会性执行和保证执行组合的后台工作的架构组件。机会性执行意味着 WorkManager 会尽快执行你的后台工作。保证执行意味着 WorkManager 会处理各种情况下的工作启动逻辑,即使你离开了应用也会启动。

WorkManager 是一个极其灵活的库,具有许多额外的优势。其中一些优势包括:

  • 支持异步的一次性和周期性任务。
  • 支持约束,例如网络条件、存储空间和充电状态。
  • 复杂工作请求的链式连接,例如并行运行工作。
  • 一个工作请求的输出用作下一个工作请求的输入。
  • 处理 API 级别兼容性,可回溯到 API 级别 14(请参阅注意)。
  • 配合或不配合 Google Play 服务工作。
  • 遵循系统健康最佳实践。
  • 支持轻松在应用 UI 中显示工作请求状态。

5. 何时使用 WorkManager

对于你需要完成的任务,WorkManager 库是一个不错的选择。这些任务的运行不依赖于工作入队后应用继续运行。即使应用关闭或用户返回主屏幕,任务也会运行。

WorkManager 的一些良好用例示例:

  • 定期查询最新新闻报道。
  • 对图片应用滤镜,然后保存图片。
  • 定期将本地数据与网络同步。

WorkManager 是在主线程外运行任务的一种选择,但它并不能涵盖所有类型的非主线程任务。协程是之前 Codelab 中讨论的另一种选择。

有关何时使用 WorkManager 的更多详细信息,请参阅后台工作指南

6. 将 WorkManager 添加到你的应用中

WorkManager 需要以下 gradle 依赖项。这已包含在构建文件中:

app/build.gradle.kts

dependencies {
    // WorkManager dependency
    implementation("androidx.work:work-runtime-ktx:2.8.1")
}

你的应用中必须使用 work-runtime-ktx 的最新 稳定版本

如果你更改了版本,请确保点击 Sync Now 以将你的项目与更新后的 gradle 文件同步。

7. WorkManager 基础知识

你需要了解一些 WorkManager 类:

  • Worker / CoroutineWorker:Worker 是一个在后台线程上同步执行工作的类。由于我们关注异步工作,我们可以使用 CoroutineWorker,它与 Kotlin Coroutines 具有互操作性。在此应用中,你将从 CoroutineWorker 类继承并覆盖 doWork() 方法。此方法是你在后台执行实际工作的代码所在之处。
  • WorkRequest:此类代表执行某些工作的请求。WorkRequest 是你定义 Worker 需要运行一次还是定期运行的地方。还可以对 WorkRequest 放置约束,要求在工作运行之前满足特定条件。一个示例是在启动请求的工作之前设备正在充电。你将 CoroutineWorker 作为创建 WorkRequest 的一部分传递进去。
  • WorkManager:此类实际上会安排你的 WorkRequest 并使其运行。它以一种分散系统资源负载的方式安排 WorkRequest,同时遵循你指定的约束。

在你的案例中,你定义了一个新的 BlurWorker 类,其中包含模糊图片的代码。当你点击开始按钮时,WorkManager 会创建一个 WorkRequest 对象并将其入队。

8. 创建 BlurWorker

在此步骤中,你将使用 res/drawable 文件夹中名为 android_cupcake.png 的图片,并在后台对其运行一些函数。这些函数会模糊图片。

  1. 在 Android 项目窗格中,右键点击包 com.example.bluromatic.workers,然后选择 New -> Kotlin Class/File
  2. 将新的 Kotlin 类命名为 BlurWorker。继承自 CoroutineWorker 并带有所需的构造函数参数。

workers/BlurWorker.kt

import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import android.content.Context

class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
}

BlurWorker 类继承自 CoroutineWorker 类,而不是更通用的 Worker 类。CoroutineWorker 类对 doWork() 的实现是一个挂起函数,这使得它可以运行 Worker 无法执行的异步代码。正如指南WorkManager 中的线程中所述,“对于 Kotlin 用户,推荐的实现是 CoroutineWorker。”

此时,Android Studio 会在 class BlurWorker 下方绘制一条红色波浪线,表示有错误。

9e96aa94f82c6990.png

如果将光标放在文本 class BlurWorker 上,IDE 会弹出一个包含错误附加信息的窗口。

cdc4bbefa7a9912b.png

错误消息指示你未按要求覆盖 doWork() 方法。

doWork() 方法中,编写代码来模糊显示的纸杯蛋糕图片。

按照以下步骤修复错误并实现 doWork() 方法:

  1. 点击文本“BlurWorker”,将光标放在类代码内部。
  2. 从 Android Studio 菜单中,选择 Code > Override Methods...
  3. Override Members 弹出窗口中,选择 doWork()
  4. 点击 OK

8f495f0861ed19ff.png

  1. 在类声明之前,立即创建一个名为 TAG 的变量,并为其赋值 BlurWorker。请注意,此变量与 doWork() 方法没有特别关系,但你稍后会在调用 Log() 时使用它。

workers/BlurWorker.kt

private const val TAG = "BlurWorker"

class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
... 
  1. 为了更好地查看工作何时执行,你需要使用 WorkerUtilmakeStatusNotification() 函数。此函数可让你轻松在屏幕顶部显示通知横幅。

doWork() 方法内部,使用 makeStatusNotification() 函数显示状态通知,并通知用户模糊 Worker 已启动并正在模糊图片。

workers/BlurWorker.kt

import com.example.bluromatic.R
...
override suspend fun doWork(): Result {

    makeStatusNotification(
        applicationContext.resources.getString(R.string.blurring_image),
        applicationContext
    )
...
  1. 添加一个 return try...catch 代码块,实际的模糊图片工作将在此处执行。

workers/BlurWorker.kt

...
        makeStatusNotification(
            applicationContext.resources.getString(R.string.blurring_image),
            applicationContext
        )

        return try {
        } catch (throwable: Throwable) {
        }
...
  1. try 块中,添加对 Result.success() 的调用。
  2. catch 块中,添加对 Result.failure() 的调用。

workers/BlurWorker.kt

...
        makeStatusNotification(
            applicationContext.resources.getString(R.string.blurring_image),
            applicationContext
        )

        return try {
            Result.success()
        } catch (throwable: Throwable) {
            Result.failure()
        }
...
  1. try 块中,创建一个名为 picture 的新变量,并通过调用 BitmapFactory.decodeResource() 方法填充该变量,并将应用的资源包和纸杯蛋糕图片的资源 ID 作为参数传入。

workers/BlurWorker.kt

...
        return try {
            val picture = BitmapFactory.decodeResource(
                applicationContext.resources,
                R.drawable.android_cupcake
            )

            Result.success()
...
  1. 通过调用 blurBitmap() 函数并传入 picture 变量和 blurLevel 参数的值 1(一)来模糊位图。
  2. 将结果保存在一个名为 output 的新变量中。

workers/BlurWorker.kt

...
            val picture = BitmapFactory.decodeResource(
                applicationContext.resources,
                R.drawable.android_cupcake
            )

            val output = blurBitmap(picture, 1)

            Result.success()
...
  1. 创建一个新变量 outputUri,并通过调用 writeBitmapToFile() 函数填充它。
  2. 在对 writeBitmapToFile() 的调用中,将应用上下文和 output 变量作为参数传入。

workers/BlurWorker.kt

...
            val output = blurBitmap(picture, 1)

            // Write bitmap to a temp file
            val outputUri = writeBitmapToFile(applicationContext, output)

            Result.success()
...
  1. 添加代码以显示包含 outputUri 变量的通知消息给用户。

workers/BlurWorker.kt

...
            val outputUri = writeBitmapToFile(applicationContext, output)

            makeStatusNotification(
                "Output is $outputUri",
                applicationContext
            )

            Result.success()
...
  1. catch 块中,记录错误消息,指示在尝试模糊图片时发生错误。对 Log.e() 的调用会传递之前定义的 TAG 变量、适当的消息和抛出的异常。

workers/BlurWorker.kt

...
        } catch (throwable: Throwable) {
            Log.e(
                TAG,
                applicationContext.resources.getString(R.string.error_applying_blur),
                throwable
            )
            Result.failure()
        }
...

默认情况下,CoroutineWorkerDispatchers.Default 运行,但可以通过调用 withContext() 并传入所需的调度程序来更改。

  1. 创建一个 withContext() 块。
  2. 在对 withContext() 的调用内部,传入 Dispatchers.IO,以便 lambda 函数在用于潜在阻塞 IO 操作的特殊线程池中运行。
  3. 将之前编写的 return try...catch 代码移到此块中。
...
        return withContext(Dispatchers.IO) {

            return try {
                // ...
            } catch (throwable: Throwable) {
                // ...
            }
        }
...

Android Studio 会显示以下错误,因为你不能在 lambda 函数内部调用 return

2d81a484b1edfd1d.png

通过添加如弹出窗口所示的标签可以修复此错误。

...
            //return try {
            return@withContext try {
...

由于此 Worker 运行非常快速,建议在代码中添加延迟以模拟运行较慢的工作。

  1. withContext() lambda 内部,添加对 delay() 实用函数 的调用,并传入 DELAY_TIME_MILLIS 常量。此调用仅用于 Codelab,以便在通知消息之间提供延迟。
import com.example.bluromatic.DELAY_TIME_MILLIS
import kotlinx.coroutines.delay

...
        return withContext(Dispatchers.IO) {

            // This is an utility function added to emulate slower work.
            delay(DELAY_TIME_MILLIS)


                val picture = BitmapFactory.decodeResource(
...

9. 更新 WorkManagerBluromaticRepository

存储库处理与 WorkManager 的所有交互。此结构遵循关注点分离设计原则,是推荐的 Android 架构模式。

  • data/WorkManagerBluromaticRepository.kt 文件中的 WorkManagerBluromaticRepository 类内部,创建一个名为 workManager 的私有变量,并通过调用 WorkManager.getInstance(context) 将一个 WorkManager 实例存储在其中。

data/WorkManagerBluromaticRepository.kt

import androidx.work.WorkManager
...
class WorkManagerBluromaticRepository(context: Context) : BluromaticRepository {

    // New code
    private val workManager = WorkManager.getInstance(context)
...

在 WorkManager 中创建 WorkRequest 并入队

好的,是时候创建 WorkRequest 并告诉 WorkManager 运行它了!有两种类型的 WorkRequest

  • OneTimeWorkRequest:只执行一次的 WorkRequest
  • PeriodicWorkRequest:按周期重复执行的 WorkRequest

你只想在点击开始按钮时模糊图片一次。

此工作发生在 applyBlur() 方法中,你在点击开始按钮时会调用它。

以下步骤在 applyBlur() 方法内部完成。

  1. 通过为模糊 Worker 创建 OneTimeWorkRequest 并调用 WorkManager KTX 中的 OneTimeWorkRequestBuilder 扩展函数,填充一个名为 blurBuilder 的新变量。

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.BlurWorker
import androidx.work.OneTimeWorkRequestBuilder
...
override fun applyBlur(blurLevel: Int) {
    // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
}
  1. 通过在 workManager 对象上调用 enqueue() 方法来启动工作。

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.BlurWorker
import androidx.work.OneTimeWorkRequestBuilder
...
override fun applyBlur(blurLevel: Int) {
    // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()

    // Start the work
    workManager.enqueue(blurBuilder.build())
}
  1. 运行应用,并在点击开始按钮时查看通知。

此时,无论你选择哪个选项,图片都模糊相同的程度。在后续步骤中,模糊量会根据所选选项而改变。

f2b3591b86d1999d.png

要确认图片成功模糊,你可以在 Android Studio 中打开 设备文件浏览器

6bc555807e67f5ad.png

然后导航到 data > data > com.example.bluromatic > files > blur_filter_outputs > <URI>,并确认纸杯蛋糕图片确实已模糊

fce43c920a61a2e3.png

10. 输入数据和输出数据

模糊资源目录中的图片资源固然不错,但要让 Blur-O-Matic 真正成为它注定要成为的革命性图片编辑应用,你需要让用户模糊屏幕上看到的图片,然后向他们显示模糊结果。

为此,我们将显示的纸杯蛋糕图片的 URI 作为输入提供给我们的 WorkRequest,然后使用我们的 WorkRequest 的输出显示最终模糊的图片。

ce8ec44543479fe5.png

输入和输出通过 Data 对象传入和传出 Worker。Data 对象是轻量级的键/值对容器。它们旨在存储少量可能从 WorkRequest 传递进出 Worker 的数据。

在下一步中,你将通过创建输入数据对象将 URI 传递给 BlurWorker

创建输入数据对象

  1. data/WorkManagerBluromaticRepository.kt 文件中的 WorkManagerBluromaticRepository 类内部,创建一个名为 imageUri 的新私有变量。
  2. 通过调用上下文方法 getImageUri() 填充该变量。

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.getImageUri
...
class WorkManagerBluromaticRepository(context: Context) : BluromaticRepository {

    private var imageUri: Uri = context.getImageUri() // <- Add this
    private val workManager = WorkManager.getInstance(context)
...

应用代码包含用于创建输入数据对象的 createInputDataForWorkRequest() 辅助函数。

data/WorkManagerBluromaticRepository.kt

// For reference - already exists in the app
private fun createInputDataForWorkRequest(blurLevel: Int, imageUri: Uri): Data {
    val builder = Data.Builder()
    builder.putString(KEY_IMAGE_URI, imageUri.toString()).putInt(BLUR_LEVEL, blurLevel)
    return builder.build()
}

首先,辅助函数创建一个 Data.Builder 对象。然后,它将 imageUriblurLevel 作为键/值对放入其中。然后通过调用 return builder.build() 创建并返回一个 Data 对象。

  1. 要为 WorkRequest 设置输入数据对象,你需要调用 blurBuilder.setInputData() 方法。你可以通过调用 createInputDataForWorkRequest() 辅助函数作为参数,一步创建并传递数据对象。对于对 createInputDataForWorkRequest() 函数的调用,传入 blurLevel 变量和 imageUri 变量。

data/WorkManagerBluromaticRepository.kt

override fun applyBlur(blurLevel: Int) {
     // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()

    // New code for input data object
    blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))

    workManager.enqueue(blurBuilder.build())
}

访问输入数据对象

现在让我们更新 BlurWorker 类中的 doWork() 方法,以获取通过输入数据对象传入的 URI 和模糊级别。如果未提供 blurLevel 的值,则默认值为 1

doWork() 方法内部:

  1. 创建一个名为 resourceUri 的新变量,并通过调用 inputData.getString() 并传入创建输入数据对象时用作键的常量 KEY_IMAGE_URI 来填充该变量。

val resourceUri = inputData.getString(KEY_IMAGE_URI)

  1. 创建一个名为 blurLevel 的新变量。通过调用 inputData.getInt() 并传入创建输入数据对象时用作键的常量 BLUR_LEVEL 来填充该变量。如果此键/值对尚未创建,请提供默认值 1(一)。

workers/BlurWorker.kt

import com.example.bluromatic.KEY_BLUR_LEVEL
import com.example.bluromatic.KEY_IMAGE_URI
...
override fun doWork(): Result {

    // ADD THESE LINES
    val resourceUri = inputData.getString(KEY_IMAGE_URI)
    val blurLevel = inputData.getInt(KEY_BLUR_LEVEL, 1)

    // ... rest of doWork()
}

有了 URI 后,现在来模糊屏幕上的纸杯蛋糕图片。

  1. 检查 resourceUri 变量是否已填充。如果未填充,你的代码应抛出异常。下面的代码使用 require() 语句,如果第一个参数评估为 false,则会抛出 IllegalArgumentException

workers/BlurWorker.kt

return@withContext try {
    // NEW code
    require(!resourceUri.isNullOrBlank()) {
        val errorMessage =
            applicationContext.resources.getString(R.string.invalid_input_uri)
            Log.e(TAG, errorMessage)
            errorMessage
    }

由于图片源是作为 URI 传入的,我们需要一个 ContentResolver 对象来读取 URI 指向的内容。

  1. 将一个 contentResolver 对象添加到 applicationContext 值。

workers/BlurWorker.kt

...
    require(!resourceUri.isNullOrBlank()) {
        // ...
    }
    val resolver = applicationContext.contentResolver
...
  1. 由于图片源现在是传入的 URI,请使用 BitmapFactory.decodeStream() 而不是 BitmapFactory.decodeResource() 来创建 Bitmap 对象。

workers/BlurWorker.kt

import android.net.Uri
...
//     val picture = BitmapFactory.decodeResource(
//         applicationContext.resources,
//         R.drawable.android_cupcake
//     )

    val resolver = applicationContext.contentResolver

    val picture = BitmapFactory.decodeStream(
        resolver.openInputStream(Uri.parse(resourceUri))
    )
  1. 在对 blurBitmap() 函数的调用中传入 blurLevel 变量。

workers/BlurWorker.kt

//val output = blurBitmap(picture, 1)
val output = blurBitmap(picture, blurLevel)

创建输出数据对象

现在你已完成此 Worker 的工作,可以在 Result.success() 中将输出 URI 作为输出数据对象返回。将输出 URI 作为输出数据对象提供,可以轻松供其他 Worker 进行进一步操作访问。当你创建 Worker 链时,此方法在下一部分中会很有用。

为此,请完成以下步骤:

  1. Result.success() 代码之前,创建一个名为 outputData 的新变量。
  2. 通过调用 workDataOf() 函数并使用常量 KEY_IMAGE_URI 作为键,使用变量 outputUri 作为值来填充此变量。workDataOf() 函数从传入的键和值对创建一个 Data 对象。

workers/BlurWorker.kt

import androidx.work.workDataOf
// ...
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
  1. 更新 Result.success() 代码,使其将此新的 Data 对象作为参数。

workers/BlurWorker.kt

//Result.success()
Result.success(outputData)
  1. 删除显示通知的代码,因为它不再需要,因为输出 Data 对象现在使用 URI。

workers/BlurWorker.kt

// REMOVE the following notification code
//makeStatusNotification(
//    "Output is $outputUri",
//    applicationContext
//)

运行应用

此时,当你运行应用时,它应该会编译。你可以通过设备文件浏览器查看模糊的图片,但尚未在屏幕上看到。

请注意,你可能需要同步才能看到图片。

a658ad6e65f0ce5d.png

太棒了!你已使用 WorkManager 模糊了一张输入图片!

11. 链式连接工作

目前,你只执行一个工作任务——模糊图片。这个任务是很棒的第一步,但应用仍然缺少一些核心功能:

  • 应用未清理临时文件。
  • 应用实际上并未将图片保存到永久文件。
  • 应用总是以相同的程度模糊图片。

你可以使用 WorkManager 工作链来添加此功能。WorkManager 允许你创建可以按顺序或并行运行的单独 WorkerRequest

在此部分中,你将创建如下所示的工作链:

c883bea5a5beac45.png

方框代表 WorkRequest

链式连接的另一个功能是它能够接受输入并产生输出。一个 WorkRequest 的输出成为链中下一个 WorkRequest 的输入。

你已经有一个用于模糊图片的 CoroutineWorker,但你还需要一个用于清理临时文件的 CoroutineWorker 和一个用于永久保存图片的 CoroutineWorker

创建 CleanupWorker

CleanupWorker 删除临时文件(如果存在)。

  1. 在 Android 项目窗格中,右键点击包 com.example.bluromatic.workers,然后选择 New -> Kotlin Class/File
  2. 将新的 Kotlin 类命名为 CleanupWorker
  3. 复制 CleanupWorker.kt 的代码,如以下代码示例所示。

由于文件操作超出了本 Codelab 的范围,你可以复制以下 CleanupWorker 代码。

workers/CleanupWorker.kt

package com.example.bluromatic.workers

import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.example.bluromatic.DELAY_TIME_MILLIS
import com.example.bluromatic.OUTPUT_PATH
import com.example.bluromatic.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File

/**
 * Cleans up temporary files generated during blurring process
 */
private const val TAG = "CleanupWorker"

class CleanupWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {

    override suspend fun doWork(): Result {
        /** Makes a notification when the work starts and slows down the work so that it's easier
         * to see each WorkRequest start, even on emulated devices
         */
        makeStatusNotification(
            applicationContext.resources.getString(R.string.cleaning_up_files),
            applicationContext
        )

        return withContext(Dispatchers.IO) {
            delay(DELAY_TIME_MILLIS)

            return@withContext try {
                val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
                if (outputDirectory.exists()) {
                    val entries = outputDirectory.listFiles()
                    if (entries != null) {
                        for (entry in entries) {
                            val name = entry.name
                            if (name.isNotEmpty() && name.endsWith(".png")) {
                                val deleted = entry.delete()
                                Log.i(TAG, "Deleted $name - $deleted")
                            }
                        }
                    }
                }
                Result.success()
            } catch (exception: Exception) {
                Log.e(
                    TAG,
                    applicationContext.resources.getString(R.string.error_cleaning_file),
                    exception
                )
                Result.failure()
            }
        }
    }
}

创建 SaveImageToFileWorker

SaveImageToFileWorker 类将临时文件保存到永久文件。

SaveImageToFileWorker 接受输入和输出。输入是一个 String,表示临时模糊图片的 URI,使用键 KEY_IMAGE_URI 存储。输出是一个 String,表示保存的模糊图片的 URI,使用键 KEY_IMAGE_URI 存储。

de0ee97cca135cf8.png

  1. 在 Android 项目窗格中,右键点击包 com.example.bluromatic.workers,然后选择 New -> Kotlin Class/File
  2. 将新的 Kotlin 类命名为 SaveImageToFileWorker
  3. 复制 SaveImageToFileWorker.kt 代码,如以下示例代码所示。

由于文件操作超出了本 Codelab 的范围,你可以复制以下 SaveImageToFileWorker 代码。在提供的代码中,注意 resourceUrioutput 值如何使用键 KEY_IMAGE_URI 进行检索和存储。此过程与你之前编写的输入和输出数据对象代码非常相似。

workers/SaveImageToFileWorker.kt

package com.example.bluromatic.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.example.bluromatic.DELAY_TIME_MILLIS
import com.example.bluromatic.KEY_IMAGE_URI
import com.example.bluromatic.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Date

/**
 * Saves the image to a permanent file
 */
private const val TAG = "SaveImageToFileWorker"

class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {

    private val title = "Blurred Image"
    private val dateFormatter = SimpleDateFormat(
        "yyyy.MM.dd 'at' HH:mm:ss z",
        Locale.getDefault()
    )

    override suspend fun doWork(): Result {
        // Makes a notification when the work starts and slows down the work so that
        // it's easier to see each WorkRequest start, even on emulated devices
        makeStatusNotification(
            applicationContext.resources.getString(R.string.saving_image),
            applicationContext
        )

        return withContext(Dispatchers.IO) {
            delay(DELAY_TIME_MILLIS)

            val resolver = applicationContext.contentResolver
            return@withContext try {
                val resourceUri = inputData.getString(KEY_IMAGE_URI)
                val bitmap = BitmapFactory.decodeStream(
                    resolver.openInputStream(Uri.parse(resourceUri))
                )
                val imageUrl = MediaStore.Images.Media.insertImage(
                    resolver, bitmap, title, dateFormatter.format(Date())
                )
                if (!imageUrl.isNullOrEmpty()) {
                    val output = workDataOf(KEY_IMAGE_URI to imageUrl)

                    Result.success(output)
                } else {
                    Log.e(
                        TAG,
                        applicationContext.resources.getString(R.string.writing_to_mediaStore_failed)
                    )
                    Result.failure()
                }
            } catch (exception: Exception) {
                Log.e(
                    TAG,
                    applicationContext.resources.getString(R.string.error_saving_image),
                    exception
                )
                Result.failure()
            }
        }
    }
}

创建工作链

目前,代码仅创建并运行单个 WorkRequest

在此步骤中,你将修改代码以创建和执行 WorkRequest 链,而不仅仅是一个模糊图片请求。

在 WorkRequest 链中,你的第一个工作请求是清理临时文件。

  1. 调用 workManager.beginWith(),而不是调用 OneTimeWorkRequestBuilder

调用 beginWith() 方法会返回一个 WorkContinuation 对象,并使用链中的第一个工作请求创建 WorkRequest 链的起始点。

data/WorkManagerBluromaticRepository.kt

import androidx.work.OneTimeWorkRequest
import com.example.bluromatic.workers.CleanupWorker
// ...
    override fun applyBlur(blurLevel: Int) {
        // Add WorkRequest to Cleanup temporary images
        var continuation = workManager.beginWith(OneTimeWorkRequest.from(CleanupWorker::class.java))

        // Add WorkRequest to blur the image
        val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
...

你可以通过调用 then() 方法并传入一个 WorkRequest 对象来添加到此工作请求链。

  1. 移除只将一个工作请求入队的 workManager.enqueue(blurBuilder.build()) 调用。
  2. 通过调用 .then() 方法将下一个工作请求添加到链中。

data/WorkManagerBluromaticRepository.kt

...
//workManager.enqueue(blurBuilder.build())

// Add the blur work request to the chain
continuation = continuation.then(blurBuilder.build())
...
  1. 创建保存图片的工作请求并将其添加到链中。

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.SaveImageToFileWorker

...
continuation = continuation.then(blurBuilder.build())

// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
    .build()
continuation = continuation.then(save)
...
  1. 要在延续对象上启动工作,请调用 enqueue() 方法。

data/WorkManagerBluromaticRepository.kt

...
continuation = continuation.then(save)

// Start the work
continuation.enqueue()
...

此代码生成并运行以下 WorkRequest 链:一个 CleanupWorker WorkRequest,后跟一个 BlurWorker WorkRequest,再后跟一个 SaveImageToFileWorker WorkRequest

  1. 运行应用。

现在你可以点击开始,并在不同 Worker 执行时看到通知。你仍然可以在设备文件浏览器中看到模糊的图片,在下一节中,你将添加一个额外的按钮,以便用户可以在设备上看到模糊的图片。

在以下屏幕截图中,请注意通知消息显示当前正在运行的 Worker。

bbe0fdd79e3bca27.png

5d43bbfff1bfebe5.png

da2d31fa3609a7b1.png

请注意,输出文件夹包含多个模糊图片——中间模糊阶段的图片以及显示所选模糊量的最终图片。

做得太棒了!现在,你可以清理临时文件、模糊图片并保存了!

12. 获取解决方案代码

要下载已完成 Codelab 的代码,可以使用以下命令:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git
$ cd basic-android-kotlin-compose-training-workmanager
$ git checkout intermediate

或者,你可以将仓库下载为 ZIP 文件,解压缩并在 Android Studio 中打开。

如果你想查看此 Codelab 的解决方案代码,请在 GitHub 上查看。

13. 总结

恭喜!你已完成 Blur-O-Matic 应用,并在此过程中了解了:

  • 将 WorkManager 添加到项目中
  • 安排 OneTimeWorkRequest
  • 输入和输出参数
  • WorkRequest 链式连接

WorkManager 支持的功能远不止本 Codelab 涵盖的范围,包括重复工作、测试支持库、并行工作请求和输入合并器。

要了解更多信息,请参阅使用 WorkManager 调度任务文档。