使用 WorkManager 在后台处理工作 - Kotlin

1. 简介

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

什么是 WorkManager

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

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

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

何时使用 WorkManager

有些任务,即便用户离开特定屏幕或您的应用,也需要完成。对于这些任务,WorkManager 库是不错的选择。

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

  • 上传日志
  • 对图片应用滤镜并保存图片
  • 定期将本地数据与网络同步

WorkManager 提供有保证的执行,然而并非所有任务都需要这种保证。因此,它并非运行所有非主线程任务的万全之选。如需详细了解何时使用 WorkManager,请参阅后台处理指南

构建内容

现在,智能手机的拍照功能基本都很强大。摄影师可以给神秘的事物拍一张模糊度可靠的照片,这种时代已经一去不复返了。

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

处于已完成状态的应用图片,包含一个纸杯蛋糕的占位图片、3 个用于应用于图片的模糊处理选项,以及 2 个按钮。一个按钮用于开始对图片进行模糊处理,另一个按钮用于查看经过模糊处理的图片。

点击“See File”(查看文件)后系统显示的经过模糊处理的图片。

学习内容

  • 将 WorkManager 添加到您的项目中
  • 调度简单的任务
  • 输入和输出参数
  • 链接工作
  • 唯一工作
  • 在界面中显示工作状态
  • 取消工作
  • 工作约束

所需条件

2. 准备工作

第 1 步 - 下载代码

点击下面的链接可下载此 Codelab 的所有代码:

如果愿意,您也可以从 GitHub 克隆 WorkManager Codelab:

$ git clone -b start_kotlin https://github.com/googlecodelabs/android-workmanager

第 2 步 - 运行应用

运行应用,您应该会看到下方的屏幕。

9e4707e0fbdd93c7.png

屏幕上应该会显示一些单选按钮,您可以通过这些按钮选择要对图片进行什么程度的模糊处理。按 Go(开始)按钮即可对图片进行模糊处理并保存。

截至目前,此应用不会应用任何模糊处理。

起始代码包含以下内容:

  • WorkerUtils:这个类包含对图片实际进行模糊处理所需的代码,并包含之后您会用于显示Notifications、将位图保存到文件以及减慢应用运行速度的一些便捷方法。
  • BlurActivity:* 此 activity 用于显示图片以及添加用于选择模糊程度的单选按钮。
  • BlurViewModel:*此视图模型用于存储显示 BlurActivity 所需的所有数据,也将是您使用 WorkManager 启动后台工作的类。
  • Constants:一个静态类,其中包含您在学习本 Codelab 期间会用到的一些常量。
  • res/activity_blur.xmlBlurActivity 的布局文件。

***** 您将仅在这些文件中编写代码。

3. 将 WorkManager 添加到您的应用

WorkManager 需要使用以下 Gradle 依赖项,这些依赖项已包含在 build 文件中:

app/build.gradle

dependencies {
    // WorkManager dependency
    implementation "androidx.work:work-runtime-ktx:$versions.work"
}

您应该在此处获取最新稳定版 work-runtime-ktx,并部署正确的版本。目前,最新版本为:

build.gradle

versions.work = "2.7.1"

如果您将版本更新为较新的版本,请务必立即同步,将您的项目与已更改的 gradle 文件同步。

4. 创建您的第一条 WorkRequest

在此步骤中,您将接受 res/drawable 文件夹中一张名为 android_cupcake.png 的图片,并在后台对这张图片运行一些函数。这些函数会对图片进行模糊处理,然后将图片保存到临时文件中。

WorkManager 基础知识

您需要了解以下几个 WorkManager 类:

  • Worker:此位置用于放置您希望在后台执行的实际工作的代码。您需要扩展此类并替换 doWork() 方法。
  • WorkRequest:此类表示请求执行某些工作。您将在创建 WorkRequest 的过程中传入 Worker。在创建 WorkRequest 时,您还可以指定 Constraints 等内容,例如运行 Worker 的时间。
  • WorkManager:这个类实质上可以调度 WorkRequest 并使其运行。它以一种在系统资源上分散负载的方式调度 WorkRequest,同时遵循您指定的约束条件。

在这种情况下,您将定义新的 BlurWorker,其中包含用于对图片进行模糊处理的代码。点击 Go(开始)按钮时,系统会创建一个 WorkRequest,然后通过 WorkManager 将其加入队列。

第 1 步 - 创建 BlurWorker

workers 软件包中,新建一个名为 BlurWorker 的 Kotlin 类。

第 2 步 - 添加构造函数

BlurWorker 类添加的 Worker 的依赖项:

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

第 3 步 - 替换和实现 doWork()

Worker 会对所显示的纸杯蛋糕图片进行模糊处理。

为了更好地了解何时执行工作,您将使用 WorkerUtil 的 makeStatusNotification()。使用此方法,您可以轻松地在屏幕顶部显示通知横幅。

替换 doWork() 方法,然后执行以下操作。您可以参考本部分末尾完成后的代码:

  1. 通过调用 applicationContext 属性获取 Context。将其分配给名为 appContext 的新 val。您接下来要执行的各种位图处理需要用到此参数。
  2. 使用函数 makeStatusNotification 显示状态通知,以向用户发送有关对图片进行模糊处理的通知。
  3. 利用纸杯蛋糕图片创建一个 Bitmap
val picture = BitmapFactory.decodeResource(
        appContext.resources,
        R.drawable.android_cupcake)
  1. 通过从 WorkerUtils 调用 blurBitmap 方法,获取此位图模糊处理后的版本。
  2. WorkerUtils 调用 writeBitmapToFile 方法,将该位图写入临时文件。请务必将返回的 URI 保存到局部变量。
  3. WorkerUtils 调用 makeStatusNotification 方法,以创建显示 URI 的通知。
  4. 返回 Result.success()
  5. 在 try/catch 语句中封装第 3-6 步的代码。捕获通用的 Throwable
  6. 在 catch 语句中,使用日志语句 Log.e(TAG, "Error applying blur") 输出错误消息。
  7. 然后在 catch 语句中返回 Result.failure()

此步骤的完整代码如下所示。

**BlurWorker.**kt

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.R

private const val TAG = "BlurWorker"
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

    override fun doWork(): Result {
        val appContext = applicationContext

        makeStatusNotification("Blurring image", appContext)

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

            val output = blurBitmap(picture, appContext)

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

            makeStatusNotification("Output is $outputUri", appContext)

            Result.success()
        } catch (throwable: Throwable) {
            Log.e(TAG, "Error applying blur")
            Result.failure()
        }
    }
}

第 4 步 - 在 ViewModel 中获取 WorkManager

ViewModel 中为 WorkManager 实例创建类变量:

BlurViewModel.kt

private val workManager = WorkManager.getInstance(application)

第 5 步 - 在 WorkManager 中将 WorkRequest 加入队列

好的,现在是时候设置 WorkRequest 并指示 WorkManager 运行它了。WorkRequest 有两种类型:

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

我们只希望在点击 Go(开始)按钮后对图片进行模糊处理。当用户点击 Go(开始)按钮时,系统会调用 applyBlur 方法,因此请通过 BlurWorker 创建 OneTimeWorkRequest。然后,使用 WorkManager 实例将您的 WorkRequest. 加入队列

将以下代码行添加到 BlurViewModel's applyBlur() 方法中:

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
   workManager.enqueue(OneTimeWorkRequest.from(BlurWorker::class.java))
}

第 6 步 - 运行您的代码!

运行您的代码。此代码应进行编译,并且在按下 Go(开始)按钮时,您应该会看到通知。请注意,如需查看更模糊的照片,您应该选择“More blurred”(更模糊)或“The most blurred”(最模糊)选项。

ed497b57e1f527be.png

如需确认图片是否已成功模糊,您可以在 Android Studio 中打开设备文件浏览器

cf10a1af6e84f5ff.png

然后依次转到 data(数据)> data(数据)> com.example.background > files(文件)> Blathfilterfilter_outputs> <URI>,并确认纸杯蛋糕事实上已经模糊:

e1f61035d680ba03.png

5. 添加输入和输出

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

为实现此目标,我们将提供作为输入显示在 WorkRequest 中的纸杯蛋糕图片的 URI,然后使用 WorkRequest 的输出显示最终的经过模糊处理的图片。

第 1 步 - 创建数据输入对象

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

您需要将用户图片的 URI 传入捆绑包中。该 URI 存储在名为 imageUri 的变量中。

BlurViewModel 中,创建一个名为 createInputDataForUri 的私有方法。该方法应执行以下操作:

  1. 创建一个 Data.Builder 对象。在收到请求时,导入 androidx.work.Data
  2. 如果 imageUri 是非 null URI,则使用 putString 方法将其添加到 Data 对象。该方法可获取一个键和一个值。您可以使用 Constants 类中的字符串常量 KEY_IMAGE_URI
  3. Data.Builder 对象调用 build() 以创建 Data 对象并返回。

下面是完整的 createInputDataForUri 方法:

BlurViewModel.kt

/**
 * Creates the input data bundle which includes the Uri to operate on
 * @return Data which contains the Image Uri as a String
 */
private fun createInputDataForUri(): Data {
    val builder = Data.Builder()
    imageUri?.let {
        builder.putString(KEY_IMAGE_URI, imageUri.toString())
    }
    return builder.build()
}

第 2 步 - 将数据对象传递到 WorkRequest

您将更改 BlurViewModel 中的 applyBlur 方法,以便:

  1. 创建新的 OneTimeWorkRequestBuilder
  2. 调用 setInputData,传入 createInputDataForUri 的结果。
  3. 构建 OneTimeWorkRequest
  4. 使用 WorkManager 将工作请求加入队列,以便系统将可以按照预期运行工作。

下面是完整的 applyBlur 方法:

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
    val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
            .setInputData(createInputDataForUri())
            .build()

    workManager.enqueue(blurRequest)
}

第 3 步 - 更新 BlurWorker 的 doWork() 以获取输入

现在,请更新 BlurWorkerdoWork() 方法,以获取从 Data 对象传入的 URI:

BlurWorker.kt

override fun doWork(): Result {
    val appContext = applicationContext

    // ADD THIS LINE
    val resourceUri = inputData.getString(KEY_IMAGE_URI)
    // ... rest of doWork()
}

第 4 步 - 对指定 URI 进行模糊处理

有了此 URI,我们现在对屏幕上的纸杯蛋糕图片进行模糊处理。

  1. 移除之前用于获取图片资源的代码。

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

  1. 检查以确认从传入的 Data 获取的 resourceUri 不为空。
  2. picture 变量分配给传入的图片,如下所示:

val picture = BitmapFactory.decodeStream(

appContext.contentResolver.

  `openInputStream(Uri.parse(resourceUri)))`

BlurWorker.kt

override fun doWork(): Result {
    val appContext = applicationContext

    val resourceUri = inputData.getString(KEY_IMAGE_URI)

    makeStatusNotification("Blurring image", appContext)

    return try {
        // REMOVE THIS
        //    val picture = BitmapFactory.decodeResource(
        //            appContext.resources,
        //            R.drawable.android_cupcake)

        if (TextUtils.isEmpty(resourceUri)) {
            Log.e(TAG, "Invalid input uri")
            throw IllegalArgumentException("Invalid input uri")
        }

        val resolver = appContext.contentResolver

        val picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)))

        val output = blurBitmap(picture, appContext)

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

        Result.success()
    } catch (throwable: Throwable) {
        Log.e(TAG, "Error applying blur")
        throwable.printStackTrace()
        Result.failure()
    }
}

第 5 步 - 输出临时 URI

此工作器的工作已完成,您可以在 Result.success() 中返回输出 URI。提供作为输出数据的输出 URI,以便其他工作器能够轻松访问这张临时图片,执行进一步操作。在下一章中,您将创建工作器链,届时此操作将非常有帮助。具体操作步骤如下:

  1. 像对输入进行的操作一样,创建新的 Data,并将 outputUri 存储为 String。使用相同的键,即 KEY_IMAGE_URI
  2. 使用 Result.success(Data outputData) 方法将它返回给 WorkManager。

BlurWorker.kt

doWork() 中的 Result.success() 行修改为:

val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

Result.success(outputData)

第 6 步 - 运行您的应用

此时,您应该运行应用。它应该进行编译并且行为与您通过设备文件浏览器查看经过模糊处理的图片相同,只是图片尚未显示在屏幕上。

如需检查是否存在其他经过模糊处理的图片,您可以在 Android Studio 中打开设备文件浏览器,然后转到 data/data/com.example.background/files/blur_filter_outputs/<URI>,就像上一步的操作一样。

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

7e717ffd6b3d9d52.png

太棒了!您已使用 WorkManager 对输入图像进行模糊处理!

6. 将您的工作串连成链

现在,您将执行一项工作任务:对图片进行模糊处理。这是非常不错的第一步,但缺少一些核心功能:

  • 此操作不会清理临时文件。
  • 实际上,它不会将图片保存到永久性文件中,
  • 而是始终对图片进行相同程度的模糊处理。

我们将使用 WorkManager 工作链添加此功能。

WorkManager 允许您创建按顺序运行或并行运行的单独 WorkerRequest。在此步骤中,您将创建一个如下所示的工作链:

54832b34e9c9884a.png

WorkRequest 表示为方框。

链接的另一个简洁功能是,一个 WorkRequest 的输出会成为链中下一个 WorkRequest 的输入。在每个 WorkRequest 之间传递的输入和输出均显示为蓝色文本。

第 1 步 - 创建清理和保存工作器

首先,您需要定义所需的所有 Worker 类。您已经有了用于对图片进行模糊处理的 Worker,但还需要用于清理临时文件的 Worker 以及用于永久保存图片的 Worker

请在 workers 软件包中创建两个扩展 Worker 的新类。

第一个类的名称应为 CleanupWorker,第二个类的名称应为 SaveImageToFileWorker

第 2 步 - 扩展工作器

Worker 类扩展 CleanupWorker 类。添加所需的构造函数参数。

class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
}

第 3 步 - 替换和实现 doWork() 以用于 CleanupWorker

CleanupWorker 不需要获取任何输入或传递任何输出。它只是删除临时文件(如果存在)。由于文件操作不在本 Codelab 的范围之内,因此您可以复制 CleanupWorker 的代码,如下所示:

CleanupWorker.kt

package com.example.background.workers

import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File

/**
 * Cleans up temporary files generated during blurring process
 */
private const val TAG = "CleanupWorker"
class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

    override 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("Cleaning up old temporary files", applicationContext)
        sleep()

        return 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) {
            exception.printStackTrace()
            Result.failure()
        }
    }
}

第 4 步 - 替换和实现 doWork() 以用于 SaveImageToFileWorker

SaveImageToFileWorker 将获取输入和输出。输入是使用键 KEY_IMAGE_URI 存储的 String,即暂时模糊处理的图片 URI,而输出也将是使用键 KEY_IMAGE_URI 存储的 String,即保存的模糊处理图片的 URI。

4fc29ac70fbecf85.png

此 Codelab 不涉及文件处理,因此我们在下面提供了代码。请注意,系统会使用键 KEY_IMAGE_URI 检索 resourceUrioutput 值。该代码与您在最后一步中为输入和输出编写的代码非常相似(它使用了全部相同的键)。

SaveImageToFileWorker.kt

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.workDataOf
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.KEY_IMAGE_URI
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

/**
 * Saves the image to a permanent file
 */
private const val TAG = "SaveImageToFileWorker"
class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

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

    override 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("Saving image", applicationContext)
        sleep()

        val resolver = applicationContext.contentResolver
        return 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, "Writing to MediaStore failed")
                Result.failure()
            }
        } catch (exception: Exception) {
            exception.printStackTrace()
            Result.failure()
        }
    }
}

第 5 步 - 修改 BlurWorker 通知

现在,我们有了用于将图片保存到正确文件夹的 Worker 链,我们可以使用 WorkerUtils 类中定义的 sleep() 方法减慢工作速度,以便更轻松地做到查看每个 WorkRequest 的启动情况,即使在模拟设备上也不例外。BlurWorker 的最终版本如下所示:

BlurWorker.kt

class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

override fun doWork(): Result {
    val appContext = applicationContext

    val resourceUri = inputData.getString(KEY_IMAGE_URI)

    makeStatusNotification("Blurring image", appContext)

    // ADD THIS TO SLOW DOWN THE WORKER
    sleep()
    // ^^^^

    return try {
        if (TextUtils.isEmpty(resourceUri)) {
            Timber.e("Invalid input uri")
            throw IllegalArgumentException("Invalid input uri")
        }

        val resolver = appContext.contentResolver

        val picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)))

        val output = blurBitmap(picture, appContext)

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

        val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

        Result.success(outputData)
    } catch (throwable: Throwable) {
        throwable.printStackTrace()
        Result.failure()
    }
}

第 6 步 - 创建 WorkRequest 链

您需要修改 BlurViewModelapplyBlur 方法以执行 WorkRequest 链,而不是仅执行一个请求。目前,代码如下所示:

BlurViewModel.kt

val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
        .setInputData(createInputDataForUri())
        .build()

workManager.enqueue(blurRequest)

调用 workManager.beginWith(),而不是调用 workManager.enqueue()。此调用会返回 WorkContinuation,其定义了 WorkRequest 链。您可以通过调用 then() 方法向此工作请求链中添加请求对象。例如,如果您拥有三个 WorkRequest 对象,即 workAworkBworkC,则可以编写以下代码:

// Example code, don't copy to the project
val continuation = workManager.beginWith(workA)

continuation.then(workB) // FYI, then() returns a new WorkContinuation instance
        .then(workC)
        .enqueue() // Enqueues the WorkContinuation which is a chain of work

此代码将生成并运行以下 WorkRequest 链:

bf3b82eb9fd22349.png

applyBlur 中创建一个 CleanupWorker WorkRequestBlurImage WorkRequestSaveImageToFile WorkRequest 链。将输入传递到 BlurImage WorkRequest 中。

此操作的代码如下:

BlurViewModel.kt

internal 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 blurRequest = OneTimeWorkRequest.Builder(BlurWorker::class.java)
            .setInputData(createInputDataForUri())
            .build()

    continuation = continuation.then(blurRequest)

    // Add WorkRequest to save the image to the filesystem
    val save = OneTimeWorkRequest.Builder(SaveImageToFileWorker::class.java).build()

    continuation = continuation.then(save)

    // Actually start the work
    continuation.enqueue()
}

此代码应该编译运行。现在,您应该可以点击 Go(开始)按钮,并可以在不同工作器运行时看到通知。您仍然可以在设备文件浏览器中查看经过模糊处理的图片,在下一步中,您将再添加一个按钮,以便用户可以在设备上查看经过模糊处理的图片。

在下面的屏幕截图中,您会发现通知消息中显示当前正在运行的工作器。

f0bbaf643c24488f.png 42a036f4b24adddb.png

a438421064c385d4.png

第 7 步 - 重复使用 BlurWorker

现在,我们需要添加对图片进行不同程度的模糊处理的功能。请获取传递到 applyBlur 中的 blurLevel 参数,并向链中添加多个模糊处理 WorkRequest 操作。只有第一个 WorkRequest 需要且应该获取 URI 输入。

您可以亲自尝试,然后与以下代码进行比较:

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
    // Add WorkRequest to Cleanup temporary images
    var continuation = workManager
            .beginWith(OneTimeWorkRequest
            .from(CleanupWorker::class.java))

    // Add WorkRequests to blur the image the number of times requested
    for (i in 0 until blurLevel) {
        val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()

        // Input the Uri if this is the first blur operation
        // After the first blur operation the input will be the output of previous
        // blur operations.
        if (i == 0) {
            blurBuilder.setInputData(createInputDataForUri())
        }

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

    // Add WorkRequest to save the image to the filesystem
    val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
            .build()

    continuation = continuation.then(save)

    // Actually start the work
    continuation.enqueue()
}

打开设备文件浏览器,查看经过模糊处理的图片。请注意,输出文件夹中包含多张模糊处理过的图片、处于模糊处理中间阶段的图片,以及根据您选择的模糊处理程度显示经过模糊处理的最终图片。

您的“工作”非常不错!现在,您可以对图片进行模糊处理,模糊程度多少完全由您掌控。处理后的图片非常有神秘感。

7. 确保工作不重复

现在,您已学会使用链,接下来应该掌握的是 WorkManager 的另一项强大功能 - 唯一工作链

有时,您一次只希望运行一个工作链。例如,您可能有一个可将本地数据与服务器同步的工作链 - 您可能希望先让第一批数据结束同步,然后再开始新的同步。为此,请使用 beginUniqueWork 而非 beginWith;并且要提供唯一的 String 名称。这会命名整个工作请求链,以便您一起引用和查询这些请求。

请使用 beginUniqueWork 确保对文件进行模糊处理的工作链是唯一的。传入 IMAGE_MANIPULATION_WORK_NAME 作为键。您还需要传入 ExistingWorkPolicy。选项包括 REPLACEKEEPAPPEND

您将使用 REPLACE,因为如果用户在当前图片完成之前决定对另一张图片进行模糊处理,我们需要停止当前图片并开始对新图片进行模糊处理。

用于启动唯一工作延续的代码如下:

BlurViewModel.kt

// REPLACE THIS CODE:
// var continuation = workManager
//            .beginWith(OneTimeWorkRequest
//            .from(CleanupWorker::class.java))
// WITH
var continuation = workManager
        .beginUniqueWork(
                IMAGE_MANIPULATION_WORK_NAME,
                ExistingWorkPolicy.REPLACE,
                OneTimeWorkRequest.from(CleanupWorker::class.java)
        )

现在,Blur-O-Matic 一次只会对一张图片进行模糊处理。

8. 标记和显示工作状态

本部分大量使用了 LiveData,因此,如果要充分了解您自己的情况,您应该熟悉如何使用 LiveData。LiveData 是一种具有生命周期感知能力的数据容器。

如果这是您首次使用 LiveData 或 Observable,您可以查看文档或 Android 生命周期感知型组件 Codelab

您要做的下一项重大更改是在执行工作时实际更改应用中显示的内容。

您可以通过获取保留 WorkInfo 对象的 LiveData 来获取任何 WorkRequest 的状态。WorkInfo 是一个包含 WorkRequest 当前状态详细信息的对象,其中包括:

下表显示了获取 LiveData<WorkInfo>LiveData<List<WorkInfo>> 对象的三种不同方法,以及每种方法相应的用途。

类型

WorkManager 方法

说明

使用 id 获取工作

getWorkInfoByIdLiveData

每个 WorkRequest 都有一个由 WorkManager 生成的唯一 ID;您可以用此 ID 获取适用于该确切 WorkRequest 的单个 LiveData

使用唯一链名称获取工作

getWorkInfosForUniqueWorkLiveData

如您所见,WorkRequest 可能是唯一链的一部分。这会在单个唯一 WorkRequests 链中为所有工作返回 LiveData
>

使用标记获取工作

getWorkInfosByTagLiveData

最后,您可以选择使用字符串标记任何 WorkRequest。您可以使用同一标记标记多个 WorkRequest,并将它们关联起来。这样会返回用于任何单个标记的 LiveData
>

您将标记 SaveImageToFileWorker WorkRequest,以便您可以使用 getWorkInfosByTag 获取该标记。您将使用一个标记为您的工作加上标签,而不是使用 WorkManager ID。因为如果您的用户对多张图片进行模糊处理,则所有保存的图片 WorkRequest 将具有相同的标记,而不是相同的 ID。此外,您也可以挑选标签。

请不要使用 getWorkInfosForUniqueWork,因为它将为所有模糊处理 WorkRequest 和清理 WorkRequest 返回 WorkInfo,还需要额外的逻辑来查找保存的图片 WorkRequest

第 1 步 - 标记您的工作

applyBlur 中,在创建 SaveImageToFileWorker 时,请使用 String 常量 TAG_OUTPUT 标记您的工作:

BlurViewModel.kt

val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
        .addTag(TAG_OUTPUT) // <-- ADD THIS
        .build()

第 2 步 - 获取 WorkInfo

现在您已经标记了工作,可以获取 WorkInfo

  1. BlurViewModel 中,声明一个名为 outputWorkInfos 的新类变量,该变量是 LiveData<List<WorkInfo>>
  2. BlurViewModel 中添加 init 块以使用 WorkManager.getWorkInfosByTagLiveData 获取 WorkInfo

您需要的代码如下:

BlurViewModel.kt

// New instance variable for the WorkInfo
internal val outputWorkInfos: LiveData<List<WorkInfo>>

// Modify the existing init block in the BlurViewModel class to this:
init {
    imageUri = getImageUri(application.applicationContext)
    // This transformation makes sure that whenever the current work Id changes the WorkInfo
    // the UI is listening to changes
    outputWorkInfos = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
}

第 3 步 - 显示 WorkInfo

现在您已拥有适用于 WorkInfoLiveData,可以在 BlurActivity 中进行观察。在观察器中:

  1. 检查 WorkInfo 列表是否不为 null 并且其中是否包含任何 WorkInfo 对象。如果尚未点击 Go(开始)按钮,则返回。
  2. 获取列表中的第一个 WorkInfo;只有一个标记为 TAG_OUTPUTWorkInfo,因为我们的工作链是唯一的。
  3. 使用 workInfo.state.isFinished 检查工作状态是否为已完成。
  4. 如果未完成,请调用 showWorkInProgress() 以隐藏 Go(开始)按钮并显示 Cancel Work(取消工作)按钮和进度条。
  5. 如果已完成,请调用 showWorkFinished() 以隐藏 Cancel Work(取消工作)按钮和进度条,并显示 Go(开始)按钮。

代码如下:

注意:在收到请求时,导入 androidx.lifecycle.Observer

BlurActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Observe work status, added in onCreate()
    viewModel.outputWorkInfos.observe(this, workInfosObserver())
}

// Define the observer function
private fun workInfosObserver(): Observer<List<WorkInfo>> {
    return Observer { listOfWorkInfo ->

        // Note that these next few lines grab a single WorkInfo if it exists
        // This code could be in a Transformation in the ViewModel; they are included here
        // so that the entire process of displaying a WorkInfo is in one location.

        // If there are no matching work info, do nothing
        if (listOfWorkInfo.isNullOrEmpty()) {
            return@Observer
        }

        // We only care about the one output status.
        // Every continuation has only one worker tagged TAG_OUTPUT
        val workInfo = listOfWorkInfo[0]

        if (workInfo.state.isFinished) {
            showWorkFinished()
        } else {
            showWorkInProgress()
        }
    }
}

第 4 步 - 运行您的应用

运行您的应用 - 它应该编译并运行,且现在可以在工作时显示进度条以及取消按钮:

7b70288f69050f0b.png

9. 显示最终输出

每个 WorkInfo 还有一个 getOutputData 方法,该方法可让您获取包含最终保存的图片的输出 Data 对象。在 Kotlin 中,您可以使用该语言为您生成的变量 outputData 访问此方法。每当有经过模糊处理的图片准备就绪可供显示时,便在屏幕上显示 See File(查看文件)按钮。

第 1 步 - 创建“See File”(查看文件)按钮

activity_blur.xml 布局中有一个隐藏的按钮。它位于 BlurActivity 中,名为 outputButton

BlurActivityonCreate() 中,为该按钮设置点击监听器。此操作应获取 URI,然后打开一个 activity 以查看该 URI。您可以使用以下代码:

BlurActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   // Setup view output image file button
   binding.seeFileButton.setOnClickListener {
       viewModel.outputUri?.let { currentUri ->
           val actionView = Intent(Intent.ACTION_VIEW, currentUri)
           actionView.resolveActivity(packageManager)?.run {
               startActivity(actionView)
           }
       }
   }
}

第 2 步 - 设置 URI 并显示按钮

您需要对 WorkInfo 观察器应用一些最后的调整,才能达到预期效果(这么说并没有双关语意):

  1. 如果 WorkInfo 完成,请使用 workInfo.outputData 获取输出数据。
  2. 然后获取输出 URI,请记住,它是使用 Constants.KEY_IMAGE_URI 键存储的。
  3. 如果 URI 不为空,则会正确保存;系统会显示 outputButton 并使用该 URI 对视图模型调用 setOutputUri

BlurActivity.kt

private fun workInfosObserver(): Observer<List<WorkInfo>> {
    return Observer { listOfWorkInfo ->

        // Note that these next few lines grab a single WorkInfo if it exists
        // This code could be in a Transformation in the ViewModel; they are included here
        // so that the entire process of displaying a WorkInfo is in one location.

        // If there are no matching work info, do nothing
        if (listOfWorkInfo.isNullOrEmpty()) {
            return@Observer
        }

        // We only care about the one output status.
        // Every continuation has only one worker tagged TAG_OUTPUT
        val workInfo = listOfWorkInfo[0]

        if (workInfo.state.isFinished) {
            showWorkFinished()

            // Normally this processing, which is not directly related to drawing views on
            // screen would be in the ViewModel. For simplicity we are keeping it here.
            val outputImageUri = workInfo.outputData.getString(KEY_IMAGE_URI)

            // If there is an output file show "See File" button
            if (!outputImageUri.isNullOrEmpty()) {
                viewModel.setOutputUri(outputImageUri)
                binding.seeFileButton.visibility = View.VISIBLE
            }
        } else {
            showWorkInProgress()
        }
    }
}

第 3 步 - 运行您的代码

运行您的代码。您应该会看到新的可点击的 See File(查看文件)按钮,该按钮会将您转到输出的文件:

5366222d0b4fb705.png

cd1ecc8b4ca86748.png

10. 取消工作

bc1dc9414fe2326e.png

您已添加此取消工作按钮,所以我们要添加一些代码来执行操作。借助 WorkManager,您可以使用 ID、按标记和唯一链名称取消工作。

在这种情况下,您需要按唯一链名称取消工作,因为您想要取消链中的所有工作,而不仅仅是某个特定步骤。

第 1 步 - 按名称取消工作

BlurViewModel 中,添加一个名为 cancelWork() 的新方法以取消唯一工作。在函数内,对 workManager 调用 cancelUniqueWork,并传入 IMAGE_MANIPULATION_WORK_NAME 标记。

BlurViewModel.kt

internal fun cancelWork() {
    workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}

第 2 步 - 调用取消方法

然后,使用 cancelButton 按钮调用 cancelWork

BlurActivity.kt

// In onCreate()
// Hookup the Cancel button
binding.cancelButton.setOnClickListener { viewModel.cancelWork() }

第 3 步 - 运行和取消工作

运行您的应用。它应该可以正常编译。先对图片进行模糊处理,然后点击“取消”按钮。整个链都会被取消!

dcb4ccfd261957b1.png

请注意,由于 WorkState 不再处于“FINISHED”(已完成)状态,因此工作取消后,只有“GO”(开始)按钮。

11. 工作约束

最后,很重要的一点是,WorkManager 支持 Constraints。对于 Blur-O-Matic,您将使用设备必须充电的约束条件。也就是说,您的工作请求只会在设备充电的情况下运行。

第 1 步 - 创建并添加充电约束条件

如需创建 Constraints 对象,请使用 Constraints.Builder。然后,您可以设置所需的约束条件,并使用方法 setRequiresCharging()(如下所示)将其添加到 WorkRequest

在收到请求时,导入 androidx.work.Constraints

BlurViewModel.kt

// Put this inside the applyBlur() function, above the save work request.
// Create charging constraint
val constraints = Constraints.Builder()
        .setRequiresCharging(true)
        .build()

// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
        .setConstraints(constraints)
        .addTag(TAG_OUTPUT)
        .build()
continuation = continuation.then(save)

// Actually start the work
continuation.enqueue()

第 2 步 - 使用模拟器或设备进行测试

现在您就可以运行 Blur-O-Matic 了。如果您使用的是一台设备,则可以移除或插入您的设备。在模拟器上,您可以在“Extended controls”(扩展控件)窗口中更改充电状态:

406ce044ca07169f.png

当设备不充电时,应会暂停执行 SaveImageToFileWorker,,直到您将设备插入充电。

302da5ec986ae769.png

12. 恭喜

恭喜!您已学完 Blur-O-Matic 应用的相关知识,且已了解如何执行以下操作:

  • 将 WorkManager 添加到您的项目中
  • 调度 OneTimeWorkRequest
  • 输入和输出参数
  • 将工作的 WorkRequest 链接到一起
  • 命名唯一 WorkRequest
  • 标记 WorkRequest
  • 在界面中显示 WorkInfo
  • 取消 WorkRequest
  • WorkRequest 添加约束条件

您的“工作”非常出色!如需查看代码的结束状态和所有更改,请执行以下操作:

如果愿意,您也可以从 GitHub 克隆已完成的 WorkManager 的 Codelab:

$ git clone https://github.com/googlecodelabs/android-workmanager

WorkManager 具有许多功能,远非本 Codelab 所能涵盖的,包括重复性工作、测试支持库、并行工作请求以及输入合并。如需了解详情,请参阅 WorkManager 文档或继续学习 WorkManager 高级 Codelab