使用 WorkManager 处理后台工作

1. 前言

此 Codelab 中介绍的 WorkManager 是一种具有向后兼容性且简单灵活的库,用于处理可延迟的后台工作。WorkManager 是 Android 平台上推荐的任务调度程序,用于处理可延迟的工作,同时可保证其得到执行。

前提条件

学习内容

实践内容

  • 修改起始应用以使用 WorkManager。
  • 实现工作请求来对图片进行模糊处理。
  • 将工作串联起来,实现一系列连续的工作。
  • 将数据传入和传出调度的作业。

所需条件

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

2. 应用概览

现在,智能手机的拍照功能基本都很强大。再也不是只有摄影师可以给神秘的事物拍一张模糊度可靠的照片的时代了。

在此 Codelab 中,您将使用 Blur-O-Matic,该应用可对照片进行模糊处理,并将处理后的照片保存到文件中。那是尼斯湖水怪还是玩具潜水艇?有了 Blur-O-Matic,没有人能看得出来!

通过屏幕上的单选按钮可选择要对图片模糊处理的程度。点击 Start 按钮可模糊处理并保存图片。

目前,该应用未应用任何模糊处理效果,也不会保存最终图片。

此 Codelab 将重点介绍如何向应用添加 WorkManager、创建 worker 以清理在对图片进行模糊处理时生成的临时文件、对图片进行模糊处理,以及保存图片的最终副本,并且点击 See File 按钮可查看该副本。您还将学习如何监控后台工作的状态,并相应地更新应用的界面。

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

通过屏幕上的单选按钮可选择图片的模糊处理程度。点击 Start 按钮后,应用会模糊处理并保存图片。

目前,当您点击 Start 按钮时,该应用不会应用任何模糊处理效果。

起始代码演示

在此任务中,您将熟悉项目的结构。以下列表介绍了项目中的重要文件和文件夹。

  • WorkerUtils:便捷方法,您稍后会使用这些方法显示 Notifications 和代码,以便将位图保存到文件中。
  • BlurViewModel:此视图模型会存储应用的状态并与代码库进行交互。
  • WorkManagerBluromaticRepository:用于通过 WorkManager 启动后台工作的类。
  • Constants:一个静态类,其中包含您在学习本 Codelab 期间会用到的一些常量。
  • BluromaticScreen:包含界面的可组合函数,并与 BlurViewModel 交互。可组合函数会显示图片,并包含用于选择所需模糊处理程度的单选按钮。

4. 什么是 WorkManager?

WorkManager 属于 Android Jetpack 的一部分,是一种架构组件,用于处理既需要机会性执行,又需要有保证的执行的后台工作。机会性执行意味着 WorkManager 会尽快执行您的后台工作。有保证的执行意味着 WorkManager 会负责通过逻辑保障在各种情况下启动您的工作,即使用户离开您的应用也无妨。

WorkManager 是一个极其灵活的库,具有许多其他优势。其中部分优势包括:

  • 支持异步一次性任务和定期任务。
  • 支持网络条件、存储空间和充电状态等约束条件。
  • 链接复杂的工作请求,例如并行运行工作。
  • 将来自一个工作请求的输出用作下一个工作请求的输入。
  • 处理到 API 级别 14 的 API 级别向后兼容性(请参阅备注)。
  • 无论是否使用 Google Play 服务都可以运行。
  • 遵循系统健康最佳实践。
  • 支持在应用界面中轻松显示工作请求的状态。

5. 何时使用 WorkManager

WorkManager 库非常适合您需要完成的任务。这些任务的运行不依赖于应用在工作加入队列后继续运行。即使应用已关闭或用户返回主屏幕,这些任务仍会运行。

以下是一些适合使用 WorkManager 的典型示例任务:

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

WorkManager 是在主线程以外运行任务的一种方式,但不是在主线程之外运行所有类型的任务的万全之选。协程是在之前的 Codelab 中讨论过的另一种方式。

如需详细了解何时使用 WorkManager,请参阅后台工作指南。

6. 将 WorkManager 添加到您的应用

WorkManager 需要以下 Gradle 依赖项。以下 build 文件中已包含此文件:

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 协程进行互操作。在此应用中,您将扩展 CoroutineWorker 类并替换 doWork() 方法。此方法用于放置您希望在后台执行的实际工作的代码。
  • WorkRequest:此类表示请求执行某些工作。WorkRequest 用于定义 worker 是需要运行一次还是定期运行。也可以对 WorkRequest 设置约束条件,要求在运行工作之前满足特定条件。例如,设备在开始请求的工作之前在充电。您将在创建 WorkRequest 的过程中传入 CoroutineWorker。
  • WorkManager:此类实际上会调度 WorkRequest 并使其运行。它以一种在系统资源上分散负载的方式调度 WorkRequest,同时遵循您指定的约束条件。

在本例中,您定义了一个新的 BlurWorker 类,其中包含用于模糊处理图片的代码。当您点击 Start 按钮时,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 类。doWork() 的 CoroutineWorker 类实现是一个挂起函数,从而能够运行 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. 点击确定。

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. 为了更好地了解何时执行了工作,您需要利用 WorkerUtil 的 makeStatusNotification() 函数。借助此函数,您可以轻松地在屏幕顶部显示通知横幅。

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

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 常量。此调用专门用于此 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。

您只希望在点击 Start 按钮时对图片进行模糊处理一次。

这项工作是在 applyBlur() 方法中进行的,当您点击 Start 按钮时会调用该方法。

以下步骤在 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. 运行应用,并查看点击 Start 按钮时出现的通知。

目前,无论您选择哪个选项,图片模糊处理程度都一样。在后续步骤中,模糊处理程度会根据所选选项而变化。

f2b3591b86d1999d.png

如需确认图片是否已成功模糊处理,您可以在 Android Studio 中打开 Device Explorer:

6bc555807e67f5ad.png

然后依次转到 data > data > com.example.bluromatic > files > blur_filter_outputs > <URI>,并确认纸杯蛋糕图片实际上已经过模糊处理:

fce43c920a61a2e3.png

10. 输入数据和输出数据

对资源目录中的图片资源进行模糊处理效果固然不错,但如果想让 O-M-Matic 真正成为一款革命性的图片编辑应用,您需要让用户能够模糊处理他们在屏幕上看到的图片,然后显示经过模糊处理后生成的图片。

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

ce8ec44543479fe5.png

输入和输出通过 Data 对象传入和传出 worker。Data 对象是轻量化的键值对容器。它们用于存储少量可通过 WorkRequest 传入和传出 worker 的数据。

在下一步中,您将创建输入数据对象,以将 URI 传递给 BlurWorker。

创建输入数据对象

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

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 对象。然后,将 imageUri 和 blurLevel 作为键值对放入其中。接着,在调用 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())来创建位图对象。

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 执行进一步操作。在下一部分中创建 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
//)

运行您的应用

此时,当您运行应用时,应该能够进行编译。您可以通过 Device Explorer 查看经过模糊处理的图片,但尚无法在屏幕上查看。

请注意,您可能需要点击 Synchronize 才能查看图片:

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 接受输入和输出。输入是使用键 KEY_IMAGE_URI 存储的 String,即暂时模糊处理的图片的 URI,输出是使用键 KEY_IMAGE_URI 存储的 String,即已保存的模糊处理图片的 URI。

de0ee97cca135cf8.png

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

由于文件操作不在本 Codelab 的讨论范围内,因此您可以为 SaveImageToFileWorker 复制以下代码。在提供的代码中,请注意如何使用键 KEY_IMAGE_URI 检索和存储 resourceUri 和 output 值。此过程与您之前为输入和输出数据对象编写的代码非常相似。

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. 运行应用。

您现在可以点击 Start,并在不同 worker 执行时看到通知。您仍然可以在 Device Explorer 中看到经过模糊处理的图片;在接下来的部分,您将添加一个额外的按钮,以便用户可以在设备上查看经过模糊处理的图片。

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

bbe0fdd79e3bca27.png5d43bbfff1bfebe5.pngda2d31fa3609a7b1.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 调度任务文档。