使用 WorkManager 进行后台工作

1. 开始之前

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

先决条件

您将学到什么

您将做什么

  • 修改入门应用程序以使用 WorkManager。
  • 实现一个工作请求以模糊图像。
  • 通过链接工作来实现一系列工作。
  • 将数据传入和传出正在调度的工作。

您需要什么

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

2. 应用程序概述

如今,智能手机的拍照功能几乎太棒了。过去,摄影师可能会拍出神秘事物的模糊照片。

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

屏幕上有单选按钮,您可以选择希望图像的模糊程度。单击“开始”按钮会模糊图像并保存图像。

目前,该应用程序不会在您单击“开始”按钮时应用任何模糊效果。

本 Codelab 侧重于将 WorkManager 添加到应用程序中,创建工作人员来清理模糊图像时创建的临时文件,模糊图像并保存图像的最终副本,您可以在单击“查看文件”按钮时查看该副本。您还将学习如何监控后台工作的状态并相应地更新应用程序的 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 的最新 稳定版本

如果您更改了版本,请确保单击“立即同步”以将您的项目与更新的 gradle 文件同步。

7. WorkManager 基础知识

您需要了解一些 WorkManager 类

  • Worker / CoroutineWorker:Worker 是一个在后台线程上同步执行工作的类。由于我们对异步工作感兴趣,因此我们可以使用 CoroutineWorker,它与 Kotlin 协程具有互操作性。在此应用程序中,您从 CoroutineWorker 类继承并覆盖 doWork() 方法。此方法是您放置要在后台执行的实际工作代码的位置。
  • WorkRequest:此类表示执行某项工作的请求。WorkRequest 是您定义工作人员是否需要运行一次或定期运行的地方。还可以对 WorkRequest 放置 约束,要求在运行工作之前满足某些条件。例如,在开始请求的工作之前,设备正在充电。您在创建 WorkRequest 时将 CoroutineWorker 作为参数传入。
  • WorkManager:此类实际上会安排您的 WorkRequest 并使其运行。它以一种方式安排 WorkRequest,即在系统资源负载之间进行分配,同时遵循您指定的约束。

在您的情况下,您定义了一个新的 BlurWorker 类,其中包含模糊图像的代码。当您单击“开始”按钮时,WorkManager 会创建并排队一个 WorkRequest 对象。

8. 创建 BlurWorker

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

  1. 右键单击 Android 项目窗格中的包 com.example.bluromatic.workers,然后选择“新建”->“Kotlin 类/文件”。
  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 菜单中选择 **代码 > 重写方法...**
  3. 从 **重写成员** 弹出窗口中选择 doWork()
  4. 点击 **确定**。

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() 函数显示状态通知并通知用户模糊工作已启动并且正在模糊图像。

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 变量和 1(一)的值作为 blurLevel 参数来模糊位图。
  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()
        }
...

默认情况下,CoroutineWorker 作为 Dispatchers.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 常量。此调用仅用于代码实验室,在通知消息之间提供延迟。
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 并告诉 WorkManager 运行它了!有两种类型的 WorkRequest

  • OneTimeWorkRequest:一个仅执行一次的 WorkRequest
  • PeriodicWorkRequest:一个在周期性地重复执行的 WorkRequest

您只希望在点击 **启动** 按钮时对图像进行一次模糊处理。

此工作在 applyBlur() 方法中进行,您在点击 **启动** 按钮时会调用此方法。

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

  1. 通过为模糊工作程序创建 OneTimeWorkRequest 并调用来自 WorkManager KTXOneTimeWorkRequestBuilder 扩展函数来填充名为 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 对象传入和传出工作程序。 Data 对象是键/值对的轻量级容器。它们用于存储可能从 WorkRequest 传入和传出工作程序的一小部分数据。

在下一步中,您将通过创建一个输入数据对象来将 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. applicationContext 值添加一个 contentResolver 对象。

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,可以将输出 URI 作为输出数据对象返回,位于 Result.success() 中。将输出 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,然后选择“新建”->“Kotlin 类/文件”。
  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 接受输入和输出。输入是临时模糊图像 URI 的 String,使用键 KEY_IMAGE_URI 存储。输出是保存的模糊图像 URI 的 String,使用键 KEY_IMAGE_URI 存储。

de0ee97cca135cf8.png

  1. 右键单击 Android 项目窗格中的包 com.example.bluromatic.workers,然后选择“新建”->“Kotlin 类/文件”。
  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

在此步骤中,您将修改代码以创建和执行工作链,而不是仅创建单个模糊图像请求。

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

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

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

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()
...

此代码会生成并运行以下工作链: 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 计划任务 文档。