使用 WorkManager 执行后台任务 - Java

1. 简介

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

什么是 WorkManager

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

WorkManager 是一个简单但非常灵活的库,具有许多其他优势。其中包括

  • 支持异步一次性任务和周期性任务
  • 支持网络状况、存储空间和充电状态等约束条件
  • 复杂工作请求的链式操作,包括并行运行工作
  • 将一个工作请求的输出用作下一个工作请求的输入
  • 处理 API 级别 14 及更高版本的兼容性(请参阅注释)
  • 可与或不与 Google Play 服务配合使用
  • 遵循系统健康最佳实践
  • LiveData 支持,以便轻松地在 UI 中显示工作请求状态

何时使用 WorkManager

对于即使用户离开特定屏幕或您的应用也仍然有用的任务,WorkManager 库是一个不错的选择。

一些适合使用 WorkManager 的任务示例

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

WorkManager 提供了保证执行的功能,并非所有任务都需要此功能。因此,它并不是用于在主线程之外运行每个任务的万能解决方案。有关何时使用 WorkManager 的更多详细信息,请查看 后台处理指南

您将构建的内容

如今,智能手机拍摄照片的功能几乎过于强大。摄影师再也无法轻松拍出模糊不清的神秘照片了。

在本 Codelab 中,您将开发 Blur-O-Matic 应用程序,该应用程序可以模糊照片和图像,并将结果保存到文件中。是 尼斯湖水怪还是玩具潜艇?有了 Blur-O-Matic,没人会知道。

Image of app in completed state, with a placeholder image of the cupcake, 3 options for blurriness to apply on image, and 2 buttons. One to start blurring the image, and one to see the blurred image.

Blurred image as seen after clicking 'See File'.

您将学到的内容

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

您需要的内容

如果您在任何时候遇到困难...

如果您在使用本 Codelab 时遇到任何问题,或者想查看代码的最终状态,可以使用以下链接

或者,如果您愿意,可以从 GitHub 克隆已完成的 WorkManager Codelab 代码

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

2. 设置

步骤 1 - 下载代码

点击以下链接下载本 Codelab 的所有代码

或者,如果您愿意,可以从 GitHub 克隆导航 Codelab 代码

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

步骤 2 - 运行应用

运行应用。您应该会看到以下屏幕

9e4707e0fbdd93c7.png

屏幕上应该有单选按钮,您可以选择所需的图像模糊程度。按下“开始”按钮最终将模糊并保存图像。

目前,应用不会应用任何模糊效果。

起始代码包含

  • WorkerUtils:此类包含实际模糊图像的代码,以及一些您稍后将用于显示 Notifications、将位图保存到文件以及降低应用速度的便捷方法。
  • BlurActivity:显示图像并包含用于选择模糊级别的单选按钮的 Activity。
  • BlurViewModel:此视图模型存储显示 BlurActivity 所需的所有数据。它也将是您使用 WorkManager 启动后台工作的类。
  • Constants:包含您在 Codelab 中将使用的一些常量的静态类。
  • res/activity_blur.xmlBlurActivity 的布局文件。

您将在以下类中进行代码更改:BlurActivityBlurViewModel

3. 将 WorkManager 添加到您的应用

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

app/build.gradle

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

您应该从 此处 获取最新版本的 work-runtime,并将其放入正确的版本中。目前最新版本为

build.gradle

versions.work = "2.7.1"

如果您将版本更新到较新版本,请确保立即同步以将您的项目与更改的 Gradle 文件同步。

4. 创建第一个 WorkRequest

在此步骤中,您将获取 res/drawable 文件夹中名为 android_cupcake.png 的图像,并在后台对其运行一些函数。这些函数将模糊图像并将其保存到临时文件。

WorkManager 基础知识

您需要了解一些 WorkManager 类

  • Worker:这是您放置要在后台执行的实际工作的代码的地方。您将扩展此类并覆盖 doWork() 方法。
  • WorkRequest表示执行某些工作的请求。您将传入您的 Worker 作为创建 WorkRequest 的一部分。在创建 WorkRequest 时,您还可以指定诸如 Constraints 之类的内容,以确定 Worker 应何时运行。
  • WorkManager此类实际上安排您的 WorkRequest 并使其运行。它以一种分散系统资源负载的方式安排 WorkRequest,同时遵守您指定的约束条件。

在您的情况下,您将定义一个新的 BlurWorker,其中将包含模糊图像的代码。当单击“开始”按钮时,将创建一个 WorkRequest,然后由 WorkManager 加入队列。

步骤 1 - 创建 BlurWorker

workers 包中,创建一个名为 BlurWorker 的新类。

它应该扩展 Worker

步骤 2 - 添加构造函数

BlurWorker 类添加一个构造函数

public class BlurWorker extends Worker {
    public BlurWorker(
        @NonNull Context appContext,
        @NonNull WorkerParameters workerParams) {
            super(appContext, workerParams);
    }
}

步骤 3 - 覆盖并实现 doWork()

您的 Worker 将模糊显示的纸杯蛋糕图像。

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

覆盖 doWork() 方法,然后实现以下操作

  1. 通过调用 getApplicationContext() 获取 Context。您将需要它来执行即将执行的各种位图操作。
  2. 从测试图像创建 Bitmap
Bitmap picture = BitmapFactory.decodeResource(
    applicationContext.getResources(),
    R.drawable.android_cupcake);
  1. 通过调用 WorkerUtils 中的静态 blurBitmap 方法获取位图的模糊版本。
  2. 通过调用 WorkerUtils 中的静态 writeBitmapToFile 方法将该位图写入临时文件。确保将返回的 URI 保存到局部变量。
  3. 通过调用 WorkerUtils 中的静态 makeStatusNotification 方法创建显示 URI 的通知。
  4. 返回 Result.success();
  5. 将步骤 2-6 中的代码包装在 try/catch 语句中。捕获泛型 Throwable
  6. 在 catch 语句中,发出错误日志语句:Log.e(TAG, "Error applying blur", throwable);
  7. 然后在 catch 语句中返回 Result.failure();

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

BlurWorker.java

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Log;

import com.example.background.R;

import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class BlurWorker extends Worker {
    public BlurWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }

    private static final String TAG = BlurWorker.class.getSimpleName();

    @NonNull
    @Override
    public Result doWork() {

        Context applicationContext = getApplicationContext();

        try {

            Bitmap picture = BitmapFactory.decodeResource(
                    applicationContext.getResources(),
                    R.drawable.android_cupcake);

            // Blur the bitmap
            Bitmap output = WorkerUtils.blurBitmap(picture, applicationContext);

            // Write bitmap to a temp file
            Uri outputUri = WorkerUtils.writeBitmapToFile(applicationContext, output);

            WorkerUtils.makeStatusNotification("Output is "
                    + outputUri.toString(), applicationContext);

            // If there were no errors, return SUCCESS
            return Result.success();
        } catch (Throwable throwable) {

            // Technically WorkManager will return Result.failure()
            // but it's best to be explicit about it.
            // Thus if there were errors, we're return FAILURE
            Log.e(TAG, "Error applying blur", throwable);
            return Result.failure();
        }
    }
}

步骤 4 - 在 ViewModel 中获取 WorkManager

在您的 ViewModel 中为 WorkManager 实例创建一个变量,并在 ViewModel 的构造函数中实例化它

BlurViewModel.java

private WorkManager mWorkManager;

// BlurViewModel constructor
public BlurViewModel(@NonNull Application application) {
  super(application);
  mWorkManager = WorkManager.getInstance(application);

  //...rest of the constructor
}

步骤 5 - 在 WorkManager 中加入 WorkRequest 队列

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

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

我们只希望在点击**Go**按钮时图像模糊一次。当点击**Go**按钮时会调用applyBlur方法,因此在那里从BlurWorker创建一个OneTimeWorkRequest。然后,使用你的WorkManager实例将你的WorkRequest排队。

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

BlurViewModel.java

void applyBlur(int blurLevel) {
   mWorkManager.enqueue(OneTimeWorkRequest.from(BlurWorker.class));
}

步骤 6 - 运行你的代码!

运行你的代码。它应该可以编译,并且你应该在按下**Go**按钮时看到通知。

e9d67a9b01039514.png

可选地,你可以在 Android Studio 中打开**设备文件浏览器**

267de13909ae6ce9.png

然后导航到**data>data>com.example.background>files>blur_filter_outputs><URI>**并确认鱼确实被模糊了

e1f61035d680ba03.png

5. 添加输入和输出

模糊资源目录中的图像资产固然不错,但要使 Blur-O-Matic 真正成为它注定要成为的革命性图像编辑应用程序,你应该允许用户模糊他们在屏幕上看到的图像,然后能够向他们显示模糊的结果。

为此,我们将显示为**输入**的纸杯蛋糕图像的 URI 提供给我们的WorkRequest

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

输入和输出通过Data对象传入和传出。Data对象是键值对的轻量级容器。它们旨在存储可能传入和传出WorkRequest的一小部分数据。

你将把用户的图像的 URI 传入一个 bundle 中。该 URI 存储在一个名为mImageUri的变量中。

创建一个名为createInputDataForUri的私有方法。此方法应

  1. 创建一个Data.Builder对象。
  2. 如果mImageUri是一个非空URI,则使用putString方法将其添加到Data对象中。此方法接受一个键和一个值。你可以使用Constants类中的字符串常量KEY_IMAGE_URI
  3. Data.Builder对象上调用build()以创建你的Data对象,并将其返回。

以下是已完成的createInputDataForUri方法

BlurViewModel.java

/**
 * Creates the input data bundle which includes the Uri to operate on
 * @return Data which contains the Image Uri as a String
 */
private Data createInputDataForUri() {
    Data.Builder builder = new Data.Builder();
    if (mImageUri != null) {
        builder.putString(KEY_IMAGE_URI, mImageUri.toString());
    }
    return builder.build();
}

步骤 2 - 将 Data 对象传递给 WorkRequest

你将希望更改applyBlur方法,使其

  1. 创建一个新的OneTimeWorkRequest.Builder
  2. 调用setInputData,传入createInputDataForUri的结果。
  3. 构建OneTimeWorkRequest
  4. 使用WorkManager将该请求排队。

以下是已完成的applyBlur方法

BlurViewModel.java

void applyBlur(int blurLevel) {
   OneTimeWorkRequest blurRequest =
                new OneTimeWorkRequest.Builder(BlurWorker.class)
                        .setInputData(createInputDataForUri())
                        .build();

   mWorkManager.enqueue(blurRequest);
}

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

现在让我们更新BlurWorkerdoWork()方法以获取我们从Data对象传入的 URI

BlurWorker.java

public Result doWork() {

       Context applicationContext = getApplicationContext();
        
        // ADD THIS LINE
       String resourceUri = getInputData().getString(Constants.KEY_IMAGE_URI);
         
        //... rest of doWork()
}

在你完成后续步骤之前,此变量未使用。

步骤 4 - 模糊给定的 URI

有了 URI,现在让我们模糊屏幕上纸杯蛋糕的图像。

  1. 删除之前获取图像资源的代码。

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

  1. 检查从传入的Data获得的resourceUri是否不为空。
  2. picture变量分配为传入的图像,如下所示

Bitmap picture = BitmapFactory.decodeStream(

appContext.contentResolver.

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

BlurWorker.java

public Worker.Result doWork() {
       Context applicationContext = getApplicationContext();
        
       String resourceUri = getInputData().getString(Constants.KEY_IMAGE_URI);

    try {

        // REPLACE THIS CODE:
        // Bitmap picture = BitmapFactory.decodeResource(
        //        applicationContext.getResources(),
        //        R.drawable.android_cupcake);
        // WITH
        if (TextUtils.isEmpty(resourceUri)) {
            Log.e(TAG, "Invalid input uri");
            throw new IllegalArgumentException("Invalid input uri");
        }

        ContentResolver resolver = applicationContext.getContentResolver();
        // Create a bitmap
        Bitmap picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)));
        //...rest of doWork

步骤 5 - 输出临时 URI

我们完成了这个 Worker,现在可以返回Result.success()了。我们将提供 OutputURI 作为**输出 Data**,以便其他 Worker 可以轻松访问此临时图像以进行进一步操作。这在下一章创建 Worker 链时将非常有用。为此

  1. 创建一个新的Data,就像你在输入时所做的那样,并将outputUri存储为String。使用相同的键KEY_IMAGE_URI
  2. 将其传递给WorkerResult.success()方法。

BlurWorker.java

此行应位于WorkerUtils.makeStatusNotification行之后,并替换doWork()中的Result.success()

Data outputData = new Data.Builder()
    .putString(KEY_IMAGE_URI, outputUri.toString())
    .build();
return Result.success(outputData);

步骤 6 - 运行你的应用程序

此时,你应该运行你的应用程序。它应该可以编译并具有相同的行为,你可以在设备文件浏览器中看到模糊的图像,但目前还不能在屏幕上看到。

要检查另一张模糊的图像,你可以在 Android Studio 中打开**设备文件浏览器**并导航到**data/data/com.example.background/files/blur_filter_outputs/<URI>**,就像你在上一步中所做的那样。

请注意,你可能需要**同步**才能看到你的图像

3e845e1040e0087b.png

干得好!你已经使用WorkManager模糊了输入图像!

6. 连接你的工作

现在,你正在执行一个单一的工作任务:模糊图像。这是一个很好的第一步,但缺少一些核心功能

  • 它不会清理临时文件。
  • 它实际上不会将图像保存到永久文件。
  • 它始终以相同的程度模糊图片。

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

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

54832b34e9c9884a.png

WorkRequest表示为框。

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

步骤 1 - 创建清理和保存 Worker

首先,你将定义所有需要的Worker类。你已经有一个用于模糊图像的Worker,但你还需要一个用于清理临时文件的Worker和一个用于永久保存图像的Worker

worker包中创建两个新的类,它们扩展Worker

第一个应该称为CleanupWorker,第二个应该称为SaveImageToFileWorker

步骤 2 - 添加构造函数

CleanupWorker类添加一个构造函数

public class CleanupWorker extends Worker {
    public CleanupWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }
}

步骤 3 - 覆盖并实现 CleanupWorker 的 doWork()

CleanupWorker不需要任何输入或传递任何输出。如果临时文件存在,它始终会删除它们。因为这不是关于文件操作的 codelab,所以你可以复制下面CleanupWorker的代码

CleanupWorker.java

import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.example.background.Constants;
import java.io.File;

public class CleanupWorker extends Worker {
    public CleanupWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }

    private static final String TAG = CleanupWorker.class.getSimpleName();

    @NonNull
    @Override
    public Result doWork() {
        Context applicationContext = getApplicationContext();

        // 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
        WorkerUtils.makeStatusNotification("Cleaning up old temporary files",
                applicationContext);
        WorkerUtils.sleep();

        try {
            File outputDirectory = new File(applicationContext.getFilesDir(),
                    Constants.OUTPUT_PATH);
            if (outputDirectory.exists()) {
                File[] entries = outputDirectory.listFiles();
                if (entries != null && entries.length > 0) {
                    for (File entry : entries) {
                        String name = entry.getName();
                        if (!TextUtils.isEmpty(name) && name.endsWith(".png")) {
                            boolean deleted = entry.delete();
                            Log.i(TAG, String.format("Deleted %s - %s",
                                    name, deleted));
                        }
                    }
                }
            }

            return Worker.Result.success();
        } catch (Exception exception) {
            Log.e(TAG, "Error cleaning up", exception);
            return Worker.Result.failure();
        }
    }
} 

步骤 4 - 覆盖并实现 SaveImageToFileWorker 的 doWork()

SaveImageToFileWorker将接收输入和输出。输入是使用键KEY_IMAGE_URI存储的String。输出也将是使用键KEY_IMAGE_URI存储的String

4fc29ac70fbecf85.png

由于这仍然不是关于文件操作的 codelab,因此代码如下所示,有两个TODO供你填写输入和输出的相应代码。这与你在上一步为输入和输出编写的代码非常相似(它使用了所有相同的键)。

SaveImageToFileWorker.java

import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.example.background.Constants;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class SaveImageToFileWorker extends Worker {
    public SaveImageToFileWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }

    private static final String TAG = SaveImageToFileWorker.class.getSimpleName();

    private static final String TITLE = "Blurred Image";
    private static final SimpleDateFormat DATE_FORMATTER =
            new SimpleDateFormat("yyyy.MM.dd 'at' HH:mm:ss z", Locale.getDefault());

    @NonNull
    @Override
    public Result doWork() {
        Context applicationContext = getApplicationContext();

        // 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
        WorkerUtils.makeStatusNotification("Saving image", applicationContext);
        WorkerUtils.sleep();

        ContentResolver resolver = applicationContext.getContentResolver();
        try {
            String resourceUri = getInputData()
                    .getString(Constants.KEY_IMAGE_URI);
            Bitmap bitmap = BitmapFactory.decodeStream(
                    resolver.openInputStream(Uri.parse(resourceUri)));
            String outputUri = MediaStore.Images.Media.insertImage(
                    resolver, bitmap, TITLE, DATE_FORMATTER.format(new Date()));
            if (TextUtils.isEmpty(outputUri)) {
                Log.e(TAG, "Writing to MediaStore failed");
                return Result.failure();
            }
            Data outputData = new Data.Builder()
                    .putString(Constants.KEY_IMAGE_URI, outputUri)
                    .build();
            return Result.success(outputData);
        } catch (Exception exception) {
            Log.e(TAG, "Unable to save image to Gallery", exception);
            return Worker.Result.failure();
        }
    }
}

步骤 5 - 修改 BlurWorker 通知

现在我们有一系列Worker负责将图像保存在正确的文件夹中,我们可以修改通知以在工作开始时通知用户并降低工作速度,以便更容易看到每个WorkRequest的开始,即使是在模拟设备上。最终版本的BlurWorker变为

BlurWorker.java

@NonNull
@Override
public Worker.Result doWork() {

    Context applicationContext = getApplicationContext();

    // 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
    WorkerUtils.makeStatusNotification("Blurring image", applicationContext);
    WorkerUtils.sleep();
    String resourceUri = getInputData().getString(KEY_IMAGE_URI);

    try {

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

        ContentResolver resolver = applicationContext.getContentResolver();
        // Create a bitmap
        Bitmap picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)));

        // Blur the bitmap
        Bitmap output = WorkerUtils.blurBitmap(picture, applicationContext);

        // Write bitmap to a temp file
        Uri outputUri = WorkerUtils.writeBitmapToFile(applicationContext, output);

        Data outputData = new Data.Builder()
                .putString(KEY_IMAGE_URI, outputUri.toString())
                .build();

        // If there were no errors, return SUCCESS
        return Result.success(outputData);
    } catch (Throwable throwable) {

        // Technically WorkManager will return Result.failure()
        // but it's best to be explicit about it.
        // Thus if there were errors, we're return FAILURE
        Log.e(TAG, "Error applying blur", throwable);
        return Result.failure();
    }
}

步骤 6 - 创建 WorkRequest 链

你需要修改BlurViewModelapplyBlur方法以执行WorkRequest链而不是只有一个。当前代码如下所示

BlurViewModel.java

void applyBlur(int blurLevel) {
    OneTimeWorkRequest blurRequest =
            new OneTimeWorkRequest.Builder(BlurWorker.class)
                    .setInputData(createInputDataForUri())
                    .build();

    mWorkManager.enqueue(blurRequest);
}

不要调用WorkManager.enqueue(),而是调用WorkManager.beginWith()。这将返回一个WorkContinuation,它定义了一个WorkRequest链。你可以通过调用then()方法添加到此工作请求链中,例如,如果你有三个WorkRequest对象,workAworkBworkC,你可以执行以下操作

// Example code. Don't copy to the project
WorkContinuation continuation = mWorkManager.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 WorkRequest、一个BlurImage WorkRequest和一个SaveImageToFile WorkRequest的链。将输入传递到BlurImage WorkRequest

此代码如下所示

BlurViewModel.java

void applyBlur(int blurLevel) {

    // Add WorkRequest to Cleanup temporary images
    WorkContinuation continuation =
        mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));

    // Add WorkRequest to blur the image
    OneTimeWorkRequest blurRequest = new OneTimeWorkRequest.Builder(BlurWorker.class)
                    .setInputData(createInputDataForUri())
                    .build();
    continuation = continuation.then(blurRequest);

    // Add WorkRequest to save the image to the filesystem
    OneTimeWorkRequest save =
        new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
            .build();
    continuation = continuation.then(save);

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

这应该可以编译运行。你现在应该能够点击**Go**按钮并在不同的 Worker 执行时看到通知。你仍然可以在设备文件浏览器中看到模糊的图像,并且在接下来的步骤中,你将添加一个额外的按钮,以便用户可以在设备上看到模糊的图像。

在下面的屏幕截图中,您会注意到通知消息显示了当前正在运行的哪个工作器。

f0bbaf643c24488f.png 42a036f4b24adddb.png

a438421064c385d4.png

步骤 7 - 重复 BlurWorker

是时候添加以不同程度模糊图像的功能了。获取传递给 applyBlurblurLevel 参数,并向链中添加相同数量的模糊 WorkRequest 操作。只有第一个 WorkRequest 需要并应该接收 uri 输入。

自己尝试一下,然后与下面的代码进行比较

BlurViewModel.java

void applyBlur(int blurLevel) {

    // Add WorkRequest to Cleanup temporary images
    WorkContinuation continuation = mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));

    // Add WorkRequests to blur the image the number of times requested
    for (int i = 0; i < blurLevel; i++) {
        OneTimeWorkRequest.Builder blurBuilder =
                new OneTimeWorkRequest.Builder(BlurWorker.class);

        // 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
    OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
            .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.java

// REPLACE THIS CODE:
// WorkContinuation continuation = 
// mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));
// WITH
WorkContinuation continuation = mWorkManager
                .beginUniqueWork(IMAGE_MANIPULATION_WORK_NAME,
                       ExistingWorkPolicy.REPLACE,
                       OneTimeWorkRequest.from(CleanupWorker.class));

Blur-O-Matic 现在将始终一次只模糊一张图片。

8. 标记和显示工作状态

本节大量使用了 LiveData,因此要完全理解正在发生的事情,您应该熟悉 LiveData。LiveData 是一个可观察的、生命周期感知的数据持有者。

如果您第一次使用 LiveData 或可观察对象,可以查看文档或 Android 生命周期感知组件 Codelab

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

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

下表显示了三种获取 LiveData<WorkInfo>LiveData<List<WorkInfo>> 对象的方法以及每种方法的作用。

类型

WorkManager 方法

描述

使用**id**获取工作

getWorkInfoByIdLiveData

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

使用**唯一链名称**获取工作

getWorkInfosForUniqueWorkLiveData

正如您刚刚看到的,WorkRequest 可以是唯一链的一部分。这将返回 LiveData>,用于单个唯一链中的所有工作 WorkRequests

使用**标签**获取工作

getWorkInfosByTagLiveData

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

您将标记 SaveImageToFileWorker WorkRequest,以便您可以使用 getWorkInfosByTagLiveData 获取它。您将使用标签为工作添加标签,而不是使用 WorkManager ID,因为如果您的用户模糊了多个图像,所有保存图像的 WorkRequest 将具有相同的标签,但具有相同的 ID。此外,您还可以选择标签。

您不会使用 getWorkInfosForUniqueWorkLiveData,因为这将返回所有模糊 WorkRequest 和清理 WorkRequestWorkInfo;找到保存图像 WorkRequest 需要额外的逻辑。

步骤 1 - 标记您的工作

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

BlurViewModel.java

OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
        .addTag(TAG_OUTPUT) // This adds the tag
        .build();

步骤 2 - 获取 WorkInfo

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

  1. 声明一个名为 mSavedWorkInfo 的新变量,它是一个 LiveData<List<WorkInfo>>
  2. BlurViewModel 构造函数中,使用 WorkManager.getWorkInfosByTagLiveData 获取 WorkInfo
  3. mSavedWorkInfo 添加一个 getter

您需要的代码如下

BlurViewModel.java

// New instance variable for the WorkInfo class
private LiveData<List<WorkInfo>> mSavedWorkInfo;

// Placed this code in the BlurViewModel constructor
mSavedWorkInfo = mWorkManager.getWorkInfosByTagLiveData(TAG_OUTPUT);  

// Add a getter method for mSavedWorkInfo
LiveData<List<WorkInfo>> getOutputWorkInfo() { return mSavedWorkInfo; } 

步骤 3 - 显示 WorkInfo

现在您有了 WorkInfoLiveData,您可以在 BlurActivity 中观察它。在观察者中

  1. 检查 WorkInfo 列表是否不为空,以及其中是否包含任何 WorkInfo 对象 - 如果没有,则表示尚未单击Go按钮,因此返回。
  2. 获取列表中的第一个 WorkInfo;因为我们使工作链唯一,所以只有一个 WorkInfo 标记为 TAG_OUTPUT
  3. 使用 workInfo.getState().isFinished(); 检查工作状态是否已完成。
  4. 如果尚未完成,则调用 showWorkInProgress(),该函数隐藏并显示相应的视图。
  5. 如果已完成,则调用 showWorkFinished(),该函数隐藏并显示相应的视图。

代码如下

BlurActivity.java

// Show work status, added in onCreate()
mViewModel.getOutputWorkInfo().observe(this, listOfWorkInfos -> {

    // If there are no matching work info, do nothing
    if (listOfWorkInfos == null || listOfWorkInfos.isEmpty()) {
        return;
    }

    // We only care about the first output status.
    // Every continuation has only one worker tagged TAG_OUTPUT
    WorkInfo workInfo = listOfWorkInfos.get(0);

    boolean finished = workInfo.getState().isFinished();
    if (!finished) {
        showWorkInProgress();
    } else {
        showWorkFinished();
    }
});

步骤 4 - 运行您的应用程序

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

7b70288f69050f0b.png

9. 显示最终输出

每个 WorkInfo 还有一个 getOutputData 方法,允许您获取包含最终保存图像的输出 Data 对象。让我们显示一个按钮,每当准备好显示模糊图像时,按钮上都显示查看文件

步骤 1 - 创建 mOutputUri

BlurViewModel 中为最终 URI 创建一个变量,并为其提供 getter 和 setter。要将 String 转换为 Uri,您可以使用 uriOrNull 方法。

您可以使用以下代码

BlurViewModel.java

// New instance variable for the WorkInfo
private Uri mOutputUri;

// Add a getter and setter for mOutputUri
void setOutputUri(String outputImageUri) {
    mOutputUri = uriOrNull(outputImageUri);
}

Uri getOutputUri() { return mOutputUri; }

步骤 2 - 创建“查看文件”按钮

activity_blur.xml 布局中已经有一个隐藏的按钮。它位于 BlurActivity 中,可以通过其视图绑定作为 seeFileButton 访问。

设置该按钮的点击侦听器。它应该获取 URI,然后打开一个活动以查看该 URI。您可以使用以下代码

BlurActivity.java

// Inside onCreate()

binding.seeFileButton.setOnClickListener(view -> {
    Uri currentUri = mViewModel.getOutputUri();
    if (currentUri != null) {
        Intent actionView = new Intent(Intent.ACTION_VIEW, currentUri);
        if (actionView.resolveActivity(getPackageManager()) != null) { 
            startActivity(actionView);
        }
    }
});

步骤 3 - 设置 URI 并显示按钮

您需要对 WorkInfo 观察者进行一些最终调整才能使其工作(并非有意为之)

  1. 如果 WorkInfo 已完成,请使用 workInfo.getOutputData(). 获取输出数据。
  2. 然后获取输出 URI,请记住它使用 Constants.KEY_IMAGE_URI 键存储。
  3. 然后,如果 URI 不为空,则表示已正确保存;显示 seeFileButton 并使用 uri 在视图模型上调用 setOutputUri

BlurActivity.java

// Replace the observer code we added in previous steps with this one.
// Show work info, goes inside onCreate()
mViewModel.getOutputWorkInfo().observe(this, listOfWorkInfo -> {

    // If there are no matching work info, do nothing
    if (listOfWorkInfo == null || listOfWorkInfo.isEmpty()) {
        return;
    }

    // We only care about the first output status.
    // Every continuation has only one worker tagged TAG_OUTPUT
    WorkInfo workInfo = listOfWorkInfo.get(0);

    boolean finished = workInfo.getState().isFinished();
    if (!finished) {
        showWorkInProgress();
    } else {
        showWorkFinished();
        Data outputData = workInfo.getOutputData();

        String outputImageUri = outputData.getString(Constants.KEY_IMAGE_URI);

        // If there is an output file show "See File" button
        if (!TextUtils.isEmpty(outputImageUri)) {
            mViewModel.setOutputUri(outputImageUri);
            binding.seeFileButton.setVisibility(View.VISIBLE);
        }
    }
});

步骤 4 - 运行您的代码

运行您的代码。您应该会看到新的可点击的查看文件按钮,该按钮将带您到输出的文件

5366222d0b4fb705.png

cd1ecc8b4ca86748.png

10. 取消工作

632d75e145022d14.png

您添加了此取消工作按钮,因此让我们添加代码以使其执行某些操作。使用 WorkManager,您可以使用 id、标签和唯一链名称取消工作。

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

步骤 1 - 按名称取消工作

在视图模型中,编写取消工作的方法

BlurViewModel.java

/**
 * Cancel work using the work's unique name
 */
void cancelWork() {
    mWorkManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME);
}

步骤 2 - 调用取消方法

然后,将按钮 cancelButton 连接到调用 cancelWork

BlurActivity.java

// In onCreate()
        
// Hookup the Cancel button
binding.cancelButton.setOnClickListener(view -> mViewModel.cancelWork());

步骤 3 - 运行并取消您的工作

运行您的应用程序。它应该可以正常编译。开始模糊图片,然后单击取消按钮。整个链都被取消了!

cf55bb104ed09d95.png

请注意,现在工作取消后,只有 GO 按钮,因为 WorkState 不再处于 FINISHED 状态。

11. 工作约束

最后但并非最不重要的一点是,WorkManager 支持 Constraints。对于 Blur-O-Matic,您将使用设备在保存时必须正在充电的约束条件。

步骤 1 - 创建和添加充电约束

要创建Constraints对象,可以使用Constraints.Builder。然后设置所需的约束条件并将其添加到WorkRequest中,如下所示

BlurViewModel.java

// In the applyBlur method

// Create charging constraint
Constraints constraints = new Constraints.Builder()
        .setRequiresCharging(true)
        .build();

// Add WorkRequest to save the image to the filesystem
OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
        .setConstraints(constraints) // This adds the Constraints
        .addTag(TAG_OUTPUT)
        .build();

continuation = continuation.then(save);

步骤 2 - 使用模拟器或设备测试

现在您可以运行 Blur-O-Matic。如果您在设备上,可以移除或插入设备。在模拟器上,您可以在**扩展控件窗口**中更改充电状态

406ce044ca07169f.png

当设备未充电时,它应该一直处于加载状态,直到您插入电源。

302da5ec986ae769.png

12. 祝贺

恭喜!您已完成 Blur-O-Matic 应用,并在过程中学习了以下内容:

  • 在您的项目中添加 WorkManager
  • 计划OneOffWorkRequest
  • 输入和输出参数
  • 将工作链接在一起WorkRequests
  • 命名唯一的WorkRequest
  • 标记WorkRequests
  • 在 UI 中显示WorkInfo
  • 取消WorkRequests
  • WorkRequest添加约束条件

优秀的“工作”!要查看代码的最终状态和所有更改,请查看

或者,如果您愿意,可以从 GitHub 克隆 WorkManager 的代码实验室

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

WorkManager 支持的功能远不止我们在本代码实验室中介绍的内容,包括重复性工作、测试支持库、并行工作请求和输入合并。要了解更多信息,请访问WorkManager 文档