使用 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 设置 约束条件,这些约束条件要求在工作运行之前必须满足某些条件。一个示例是在启动请求的工作之前设备正在充电。您将 CoroutineWorker 作为创建 WorkRequest 的一部分传入。
  • 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 变量和 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()
        }
...

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

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

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

您只希望在单击**开始**按钮时图像模糊一次。

此工作发生在 applyBlur() 方法中,您在单击**开始**按钮时调用此方法。

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

  1. 通过为模糊工作程序创建 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 > 并确认纸杯蛋糕图像确实已模糊

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. 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,可以将输出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

在此步骤中,您将修改代码以创建和执行一系列WorkRequests,而不仅仅是一个模糊图像请求。

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

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

调用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. 要启动工作,请在continuation对象上调用enqueue()方法。

data/WorkManagerBluromaticRepository.kt

...
continuation = continuation.then(save)

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

此代码生成并运行以下WorkRequests链:一个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
  • 输入和输出参数
  • 将工作链接在一起WorkRequests

WorkManager支持的内容远不止我们在本Codelab中介绍的内容,包括重复性工作、测试支持库、并行工作请求和输入合并器。

要了解更多信息,请访问使用WorkManager安排任务文档。