バックグラウンド処理と WorkManager - Java

Android には、遅延可能なバックグラウンド処理を行うためのさまざまな方法が用意されています。この Codelab では、遅延可能なバックグラウンド処理用のライブラリで、互換性、柔軟性、シンプルさを兼ね備えた WorkManager を取り上げます。WorkManager は、Android 上で遅延可能な処理を確実に実行するための推奨タスク スケジューラです。

WorkManager とは

WorkManager は Android Jetpack の一部であり、待機的実行と確実な実行というニーズの組み合わせをもつバックグラウンド処理のためのアーキテクチャ コンポーネントです。待機的実行とは、WorkManager がバックグラウンド処理を可能になり次第実行することを指します。確実な実行とは、たとえばアプリを終了した場合など、さまざまな状況下で WorkManager がその処理の開始ロジックを保持して実行することを指します。

WorkManager はシンプルでありながら柔軟性に優れたライブラリで、他にも多くのメリットがあります。以下に例を示します。

  • 非同期の 1 回限りのタスクと定期的なタスクの両方をサポート
  • ネットワーク状態、保存容量、充電ステータスなどの制約をサポート
  • 処理の並列実行など、複雑な処理リクエストのチェーンを作成可能
  • 処理リクエストの出力を、後続の処理リクエストの入力として使用可能
  • API レベル 14 までの下位互換性(注を参照)
  • Google Play 開発者サービスの有無に関係なく動作
  • システムの健全性に関するベスト プラクティスを遵守
  • UI に処理リクエストのステータスを簡単に表示するための LiveData のサポート

WorkManager の用途

WorkManager ライブラリの使用が適しているのは、ユーザーが特定の画面やアプリを離れた場合でも完了することが求められるタスクです。

WorkManager の使用が適したタスクの例を以下に示します。

  • ログのアップロード
  • 画像へのフィルタ適用と画像の保存
  • ローカルデータとネットワークとの定期的な同期

WorkManager は処理を確実に実行しますが、すべてのタスクがそれを必要とするとは限りません。そのため、メインスレッドから切り離されたタスクすべてに適しているわけではありません。WorkManager の用途について詳しくは、バックグラウンド処理ガイドをご覧ください。

作成するアプリの概要

最近のスマートフォンは、写真撮影の性能が良すぎるくらいです。写ったものがミステリアスに見えるほどぼやけた写真が撮れたのは、過去の話です。

この Codelab では、写真や画像にぼかしを入れて結果をファイルに保存するアプリ、Blur-O-Matic を作成します。ネッシーのような怪物か、おもちゃの潜水艦か、Blur-O-Matic を使えば、誰にもわからなくります。

雑種シマスズキ(米国農務省農業研究局、Peggy Greb 氏撮影)

学習内容

  • プロジェクトへの WorkManager の追加
  • 簡単なタスクのスケジュール設定
  • 入出力パラメータ
  • 処理チェーンの作成
  • 一意処理
  • 処理ステータスの UI への表示
  • 処理のキャンセル
  • 処理の制約

必要なもの

行き詰まった場合は

Codelab の途中で行き詰まった場合や、コードの最終状態を確認する必要が生じた場合は、次のリンクを使用してください。

最終状態のコードをダウンロード

また、必要に応じて、GitHub から完了状態の WorkManager Codelab のクローンを作成することもできます。

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

ステップ 1 - コードをダウンロードする

次のリンクをクリックして、この Codelab 用のコードすべてをダウンロードします。

初期状態のコードをダウンロード

必要に応じて、GitHub からナビゲーション Codelab のクローンを作成することもできます。

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

ステップ 2 - 画像を入手する

使用しているデバイスにすでにダウンロードまたは撮影した写真があれば、このステップは終了です。

新しいデバイス(作成したばかりのエミュレータなど)を使用している場合は、そのデバイスで写真を撮影するか、ウェブから画像をダウンロードします。何かミステリアスなものを選びましょう。

ステップ 3 - アプリを実行する

アプリを実行します。下図の画面が表示されるはずです(最初のプロンプトで、必ず写真にアクセスする権限を許可します。画像が無効になっている場合は、アプリを再度開きます)。

画像を選択して次の画面に進みます。この画面では、ラジオボタンで画像をどの程度ぼかすかを選択できます。[GO] ボタンを選択すると、最終的に画像がぼかし加工されて保存されます。

上の写真では、まだぼかしは適用されていません。

初期状態のコードには以下が含まれています。

  • WorkerUtils**:** このクラスには、実際にぼかしを入れるコードと、後で Notifications を表示したり、アプリを遅らせたりするのに使用するいくつかのメソッドが含まれています。
  • BlurActivity***:** 画像を表示し、ぼかしの程度を選択するためのラジオボタンを含むアクティビティ。
  • BlurViewModel***:** このビューモデルには、BlurActivity の表示に必要なすべてのデータが格納されています。WorkManager を使用してバックグラウンド処理を開始するクラスでもあります。
  • Constants**:** Codelab で使用する定数が含まれる静的クラス。
  • SelectImageActivity**:** 画像を選択するための最初のアクティビティ。
  • res/activity_blur.xmlres/activity_select.xml: 各アクティビティのレイアウト ファイル。

***** コードを書き込むのはこの印の付いたファイルのみです。

WorkManager には、下記の Gradle 依存関係が必要です。これはすでに次のビルドファイルに含まれています

app/build.gradle

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

こちらから work-runtime の最新バージョンを入手し、正しいバージョンを挿入してください。現時点での最新バージョンは下記のとおりです。

build.gradle

versions.work = "2.3.3"

新しいバージョンにアップデートした場合は、必ず [Sync Now] をクリックしてプロジェクトと変更された Gradle ファイルを同期してください。

この手順では、res/drawable フォルダにある test.jpg という画像に対して、いくつかの関数をバックグラウンドで実行します。これらの関数により、画像はぼかし加工され、一時ファイルに保存されます。

WorkManager の基礎

把握しておくべき WorkManager クラスとして、以下のものがあります。

  • Worker: ここに、バックグラウンドで実行する処理のコードを記述します。このクラスを拡張して doWork() メソッドをオーバーライドします。
  • WorkRequest: 処理実行のリクエストを表します。WorkRequest の作成の一環として Worker を渡します。WorkRequest を作成する際は、Worker を実行する場合についての Constraints なども指定できます。
  • WorkManager: このクラスが実際に WorkRequest をスケジュールして実行します。指定された制約を尊重しながら、負荷がシステム リソースに分散されるよう WorkRequest をスケジュールします。

今回は、画像にぼかしを入れるコードを含んだ BlurWorker を新たに定義します。[GO] ボタンを選択すると、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 で画像 res/test.jpg にぼかしを入れます。

doWork() メソッドをオーバーライドし、以下のように実装します。

  1. getApplicationContext() を呼び出して Context を取得します。これは、この後実行するさまざまなビットマップ操作で必要になります。
  2. 次のようにして、テスト画像から Bitmap を作成します。
Bitmap picture = BitmapFactory.decodeResource(
    applicationContext.getResources(),
    R.drawable.test);
  1. WorkerUtils の静的 blurBitmap メソッドを呼び出して、ぼかしの入ったビットマップを取得します。
  2. WorkerUtils の静的 writeBitmapToFile メソッドを呼び出して、このビットマップを一時ファイルに書き込みます。必ず返された URI をローカル変数に保存するようにしてください。
  3. WorkerUtils の静的 makeStatusNotification メソッドを呼び出して、URI を表示する通知を作成します。
  4. Result.success(); を返します。
  5. ステップ 2~6 のコードを try / catch ステートメントでラップします。一般的な Throwable をキャッチします。
  6. catch ステートメントで、エラー Log ステートメントを出力します(Log.e(TAG, "Error applying blur", throwable);)。
  7. 続いて 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.test);

            // 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 を含める

ViewModelWorkManager インスタンスの変数を作成し、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 には次の 2 種類があります。

  • OneTimeWorkRequest: 1 回だけ実行する WorkRequest
  • PeriodicWorkRequest: 定期的に繰り返す WorkRequest

[GO] ボタンが選択されたときに、画像にぼかしを入れるのは 1 回だけです。[GO] ボタンの選択により applyBlur メソッドが呼び出されるため、そこで BlurWorker から OneTimeWorkRequest を作成します。その後、WorkManager インスタンスを使用して WorkRequest. をキューに追加します。

次のコード行を BlurViewModel の applyBlur() メソッドに追加します。

BlurViewModel.java

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

ステップ 6 - コードを実行する

コードを実行します。[GO] ボタンを選択するとコンパイルされ、通知が表示されます。

7ef0320960f4d756.png

(任意)Android Studio で Device File Explorer を開きます。

cf10a1af6e84f5ff.png

開いたら、data>data>com.example.background>files>blur_filter_outputs><URI> に移動して、実際に魚にぼかしが入ったことを確認します。

7f5eba3559b44cbb.png

テスト画像にぼかしを入れるところまではできましたが、Blur-O-Matic を実用的な画像編集アプリにするためには、ぼかす画像をユーザーが指定できるようにする必要があります。

そのためには、ユーザーが選択した画像の URI を WorkRequest への入力として指定します。

ステップ 1 - Data 入力オブジェクトを作成する

入力と出力は、Data オブジェクトを介して渡されます。Data オブジェクトは、Key-Value ペアの軽量コンテナです。WorkRequest とやり取りする可能性のある少量データの格納を目的としています。

ここでバンドルに渡そうとしているのは、ユーザーの画像の URI です。この URI は、mImageUri という変数に格納されています。

createInputDataForUri という名前のプライベート メソッドを作成します。このメソッドは以下を動作を行います。

  1. Data.Builder オブジェクトを作成します。
  2. mImageUri が null 以外の 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() を更新する

今度は、Data オブジェクトから渡された URI を取得するよう、BlurWorkerdoWork() メソッドを更新しましょう。

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 を使えば、ユーザーが指定した画像にぼかしを入れることができます。

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.test);
        // 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() を返します。出力 Data として outputUri を提供し、以降の処理で他の Worker がこの一時画像を簡単に利用できるようにします。この URI は、次の章で Worker のチェーンを作成する際に役立ちます。手順は次のとおりです。

  1. 入力の場合と同様に新しい Data を作成し、outputUriString として格納します。キーも同じもの(KEY_IMAGE_URI)を使用します。
  2. この Data を 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 で Device File Explorer を開き、前の手順で行ったのと同様、data/data/com.example.background/files/blur_filter_outputs/<URI> を確認します。

なお、画像を表示するには、[Synchronize] が必要な場合があります。

7e717ffd6b3d9d52.png

おつかれさまでした。WorkManager を使用して入力画像にぼかしを入れることができました。

現時点のタスクで行っているのは、画像にぼかしを入れるという処理のみです。たしかにこれがなくては始まりませんが、まだ以下のように重要な機能が欠けています。

  • 一時ファイルがクリーンアップされません。
  • 画像が永続ファイルに保存されません。
  • 写真に常に同程度のぼかししか入れられません。

ここでは、WorkManager の処理チェーンを使用して上記の機能を追加します。

WorkManager を使用すると、個別に作成した WorkerRequest を順次または並列に実行できます。この手順では、下図のような処理チェーンを作成します。

54832b34e9c9884a.png

それぞれの箱は WorkRequest を表します。

チェーンのもう一つ便利な特長は、WorkRequest の出力を後続の WorkRequest の入力にできるという点です。以下、各 WorkRequest 間の入出力を青色のテキストで示します。

ステップ 1 - クリーンアップ用と保存用の Worker を作成する

まず、必要な Worker クラスをすべて定義します。画像にぼかしを入れる Worker はすでにありますが、一時ファイルをクリーンアップする Worker と、画像を永続的に保存する Worker も必要です。

worker パッケージに、Worker を拡張した 2 つの新しいクラスを作成します。

1 つ目を CleanupWorker、2 つ目を 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 になります。

475a08a82ea675ca.png

ファイル操作に関する Codelab ではないので、入力と出力のコードを正しく記述するという 2 つの 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 のチェーンを作成する

WorkRequest を単独ではなくチェーンとして実行するには、BlurViewModelapplyBlur メソッドを変更する必要があります。現時点でのコードは次のとおりです。

BlurViewModel.java

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

    mWorkManager.enqueue(blurRequest);
}

ここで、WorkManager.enqueue() の代わりに WorkManager.beginWith() を呼び出します。これにより、WorkRequest のチェーンを定義する WorkContinuation が返されます。WorkRequest をこのチェーンに追加するには、then() を呼び出します。たとえば、workAworkBworkC の 3 つの WorkRequest オブジェクトがある場合は、次のようにします。

// 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 のチェーンが生成されます。

2c4bf31e5f6522ad.png

それでは、applyBlurCleanupWorker WorkRequestBlurImage WorkRequestSaveImageToFile 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();
}

これを コンパイルして実行します。選択した画像にぼかしが入って Pictures フォルダに保存されたはずです。

e2d29f34bdf01860.png

ステップ 7 - BlurWorker を繰り返す

次は、画像に程度の異なるぼかしを加える機能を追加します。blurLevel パラメータを applyBlur に渡し、その数だけぼかし処理の 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();
}

おつかれさまでした。これで、ぼかしの度合いを選択できるようになりました。ミステリアスな画像を作成できます。

fcb326118dd99959.png

チェーンを使えるようになったので、次は WorkManager のもう一つの強力な機能である一意処理チェーンに取り組みましょう。

実行する処理チェーンを一度に 1 つにしたい場合があります。たとえば、ローカルデータとサーバーを同期する処理チェーンなら、最初のデータ同期が終わってから 2 回目を開始するのが望ましいでしょう。そのためには、beginWith の代わりに beginUniqueWork を使用し、一意の 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 がぼかしを入れる画像は一度に 1 つのみになりました。

このセクションには LiveData が何度も出てくるため、内容を完全に把握するには LiveData に習熟している必要があります。LiveData は、ライフサイクルを認識する監視可能なデータホルダーです。

LiveData や監視可能オブジェクトを初めて使用する場合は、ドキュメントまたは Android ライフサイクル対応コンポーネント Codelab をご確認ください。

次に行う大きな変更は、処理実行時にアプリに表示される内容を実際に変更することです。

WorkInfo オブジェクトを保持する LiveData を取得することにより、任意の WorkRequest のステータスを取得できます。WorkInfo は、WorkRequest の現在のステータスに関する以下の詳細情報を含むオブジェクトです。

次の表に、LiveData<WorkInfo> オブジェクトまたは LiveData<List<WorkInfo>> オブジェクトを取得する 3 種類の方法を、それぞれの説明とともに示します。

種類

WorkManager のメソッド

説明

ID を使用した処理の取得

getWorkInfoByIdLiveData

WorkRequest には WorkManager によって生成された一意の ID があります。これを使用して、該当する唯一の WorkRequestLiveData を取得できます。

一意のチェーン名を使用した処理の取得

getWorkInfosForUniqueWorkLiveData

前述のとおり、WorkRequest は一意のチェーンに含めることができます。これを使用して、WorkRequests の一意のチェーン 1 つに含まれるすべての処理の LiveData
>
を取得できます。

タグを使用した処理の取得

getWorkInfosByTagLiveData

任意の WorkRequest には、必要に応じて文字列のタグを付けることができます。複数の WorkRequest に同じタグを付けると、それらを関連付けることができます。これを使用して、任意の 1 つのタグについて LiveData
>
を取得できます。

ここでは、SaveImageToFileWorkerWorkRequest にタグを付けて、getWorkInfosByTagLiveData を使用して取得できるようにします。WorkManager ID を使用する代わりに処理にタグを付けるのは、ユーザーが複数の画像にぼかしを入れる場合、画像保存 WorkRequest のすべてに共通するのは、ID ではなくタグになるためです。また、タグは選択することもできます。

getWorkInfosForUniqueWorkLiveData を使用しないのは、これによりすべてのぼかしの WorkRequest とクリーンアップの WorkRequestWorkInfo まで返され、画像保存 WorkRequest を特定するには追加のロジックが必要になるためです。

ステップ 1 - 処理にタグを付ける

applyBlurSaveImageToFileWorker を作成するときに、String 定数 TAG_OUTPUT を使用して処理にタグを付けます。

BlurViewModel.java

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

ステップ 2 - WorkInfo を取得する

処理にタグが付いたので、WorkInfo を取得できます。

  1. LiveData<List<WorkInfo>> の新しい変数 mSavedWorkInfo を宣言します。
  2. BlurViewModel コンストラクタで、WorkManager.getWorkInfosByTagLiveData を使用して WorkInfo を取得します。
  3. mSavedWorkInfo のゲッターを追加します。

必要なコードは以下のとおりです。

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 のリストが null でなく、WorkInfo オブジェクトが含まれていることを確認します。含まれていない場合は、まだ [GO] ボタンが選択されていないため戻ります。
  2. リストの最初の WorkInfo を取得します。処理チェーンを一意にしたため、TAG_OUTPUT でタグ付けされた WorkInfo は 1 つのみになります。
  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 - アプリを実行する

アプリを実行します。コンパイルと実行がなされ、処理中は進行状況バーとキャンセル ボタンが表示されます。

b7d8d3182f91ce23.png

WorkInfo には getOutputData メソッドもあり、最後に保存された画像を含む出力 Data オブジェクトを取得できます。ぼかしを入れた画像が準備できたら、[SEE FILE] ボタンを表示しましょう。

ステップ 1 - mOutputUri を作成する

BlurViewModel で最終 URI 用の変数を作成し、そのゲッターとセッターを指定します。StringUri に変換するには、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 - SEE FILE ボタンを作成する

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 を表示するとともにビューモデルの setOutputUri をこの URI を使って呼び出します。

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 - コードを実行する

コードを実行します。[SEE FILE] ボタンが新たに表示され、選択すると出力ファイルが開くはずです。

992d0b2390600774.png

bc1dc9414fe2326e.png

[CANCEL WORK] ボタンを追加したので、これを機能させるコードも追加しましょう。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 - 処理を実行してキャンセルする

アプリを実行します。正常にコンパイルされるはずです。画像のぼかしを開始したら、キャンセル ボタンを選択します。チェーン全体がキャンセルされます。

bdaadc9bb25472cb.png

最後になりましたが、WorkManagerConstraints をサポートしていることを忘れてはいけません。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 を実行できるようになりました。デバイスを使用している場合は、電源を切断または接続します。エミュレータを使用している場合は、下図のように [Extended controls] ウィンドウで充電ステータスを変更できます。

c2e56295cbe73f8.png

デバイスが充電中でない場合、電源に接続するまで読み込み状態のままになります。

b7d8d3182f91ce23.png

これで、Blur-O-Matic アプリが完成しました。このプロセスでは以下について学びました。

  • プロジェクトへの WorkManager の追加
  • OneOffWorkRequest のスケジュール設定
  • 入出力パラメータ
  • 処理チェーンによる WorkRequest の連結
  • 一意の WorkRequest チェーンの命名
  • WorkRequest へのタグ付け
  • WorkInfo の UI への表示
  • WorkRequest のキャンセル
  • WorkRequest への制約の追加

本当におつかれさまでした。最終状態のコードとすべての変更を確認するには、以下をご覧ください。

最終状態のコードをダウンロード

または、GitHub から WorkManager の Codelab のクローンを作成することもできます。

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

WorkManager は、この Codelab で取り上げたもの以外にも、繰り返し処理、テスト支援ライブラリ、並列処理リクエスト、入力マージツールなど、多くの機能をサポートしています。詳しくは、WorkManager のドキュメントをご覧ください。