WorkManager を使用したバックグラウンド処理 - Kotlin

1. はじめに

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 を使えば、誰にもわからなくります。

完成した状態のアプリの画像。カップケーキのプレースホルダ画像、画像にぼかしを入れるための 3 つのオプション、2 つのボタンがあります。画像のぼかしを開始する画面と、ぼかしを入れた画像を表示する画面。

[SEE FILE] をクリックした後で表示される、ぼかしを入れた画像。

学習内容

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

必要なもの

2. 設定方法

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

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

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

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

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

アプリを実行します。次の画面が表示されます。

9e4707e0fbdd93c7.png

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

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

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

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

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

3. アプリに WorkManager を追加する

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

app/build.gradle

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

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

build.gradle

versions.work = "2.7.1"

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

4. 最初の WorkRequest を作成する

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

WorkManager の基礎

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

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

今回は、画像にぼかしを入れるコードを含んだ BlurWorker を新たに定義します。[GO] ボタンを選択すると、WorkRequest が作成されて WorkManager によりキューに追加されるようにします。

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

workers パッケージで、新しい Kotlin クラス BlurWorker を作成します。

ステップ 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. WorkerUtilsblurBitmap メソッドを呼び出して、ぼかしの入ったビットマップを取得します。
  2. WorkerUtilswriteBitmapToFile メソッドを呼び出して、このビットマップを一時ファイルに書き込みます。返される URI をローカル変数に保存します。
  3. WorkerUtilsmakeStatusNotification メソッドを呼び出して、URI を表示する通知を作成します。
  4. Result.success() を返します。
  5. ステップ 3~6 のコードを try / catch ステートメントでラップします。一般的な Throwable をキャッチします。
  6. catch ステートメント内で、次のような Log ステートメントを使用してエラー メッセージを出力します: Log.e(TAG, "Error applying blur")
  7. 続いて 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 には次の 2 種類があります。

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

[GO] ボタンが選択されたときに、画像にぼかしを入れるのは 1 回だけです。[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 moset blurred] オプションを選択します。

ed497b57e1f527be.png

画像のぼかし処理が正しく行われたかどうかを確認するには、Android Studio で Device File Explorer を開きます。

cf10a1af6e84f5ff.png

次に、[data] > [data] > [com.example.background] > [files] > [blur_filter_outputs] > <URI> の順に移動して、実際にカップケーキにぼかしが入ったことを確認します。

e1f61035d680ba03.png

5. 入力と出力を追加する

リソース ディレクトリ内の画像アセットにぼかしを入れることができました。しかし、Blur-O-Matic をより優れた画像編集アプリにするには、画面に表示されている画像にぼかしを入れて、その結果を画面で確認できるようにする必要があります。

そのためには、表示されるカップケーキの画像の URI を WorkRequest入力として指定し、WorkRequest の出力を使って、ぼかしを入れた最終的な画像を表示します。

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

入力と出力は、Data オブジェクトを介して渡されます。Data オブジェクトは、Key-Value ペアの軽量コンテナです。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 - Data オブジェクトを WorkRequest に渡す

BlurViewModelapplyBlur メソッドを変更して、次のことを行うようにします。

  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() を更新する

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

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 を出力する

この Worker での作業が完了し、Result.success() で出力 URI を返すことができるようになりました。出力 URI を出力データとして渡し、以降の処理で他のワーカーがこの一時画像を簡単に利用できるようにします。これは、次の章でワーカーのチェーンを作成する際に役立ちます。方法は次のとおりです。

  1. 入力の場合と同様に新しい Data を作成し、outputUriString として格納します。キーも同じもの(KEY_IMAGE_URI)を使用します。
  2. この Data を、Result.success(Data outputData) メソッドを使用して WorkManager に返します。

BlurWorker.kt

doWork() 内の Result.success() の行を次のように変更します。

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

Result.success(outputData)

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

この時点でアプリを実行します。アプリがコンパイルされます。ぼかしを入れた画像は Device File Explorer からは表示できても、画面にはまだ表示されないという同じ動作になるはずです。

ぼかしを入れた画像を確認するには、Android Studio で Device File Explorer を開き、前のステップと同様に 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 と、画像を永続的に保存する Worker も必要です。

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

1 つ目を CleanupWorker、2 つ目を SaveImageToFileWorker とします。

ステップ 2 - Worker を継承する

Worker クラスを継承した CleanupWorker クラスを作成します。必要なコンストラクタ パラメータを追加します。

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

ステップ 3 - CleanupWorker の doWork() をオーバーライドして実装する

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 - SaveImageToFileWorker の doWork() をオーバーライドして実装する

SaveImageToFileWorker は入力を受け取り、出力を渡します。入力は、ぼかしを入れた一時画像の URI の String であり、キー KEY_IMAGE_URI を使用して格納されます。出力も、保存済みのぼかしを入れた画像の URI の String であり、キー KEY_IMAGE_URI を使用して格納されます。

4fc29ac70fbecf85.png

これはファイル操作に関する Codelab ではないので、コードを以下に示します。resourceUrioutput の値がキー KEY_IMAGE_URI を使用してどのように取得されるかに注目してください。これは、前の手順で入出力のために作成したコードとよく似ています(使用するキーはまったく同じです)。

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 のチェーンができました。次に、エミュレータ デバイスでも各 WorkRequest の開始を容易に確認できるよう、WorkerUtils クラスで定義されている sleep() メソッドを使用して処理速度を遅くします。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 のチェーンを作成する

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

BlurViewModel.kt

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

workManager.enqueue(blurRequest)

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

// 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

それでは、applyBlurCleanupWorker 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] ボタンを押すと、実行中のさまざまなワーカーの通知が表示されます。ここでも、ぼかしを入れた画像は Device File Explorer で確認できます。次のステップでは、ぼかしを入れた画像をデバイスで確認できるように、別のボタンを追加します。

以下のスクリーンショットでは、現在実行されているワーカーを示す通知メッセージが表示されています。

f0bbaf643c24488f.png 42a036f4b24adddb.png

a438421064c385d4.png

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

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

Device File Explorer を開き、ぼかしを入れた画像を確認します。出力フォルダには、ぼかしが入った複数の画像が保存されています。それらは、ぼかし処理の中間ステージの画像と、選択したぼかし量に基づいてぼかしを入れた最終画像です。

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

7. 処理チェーンを一意にする

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

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

8. 処理にタグを付けてステータスを表示する

このセクションには 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 にタグを付けて、getWorkInfosByTag を使用して取得できるようにします。WorkManager ID を使用する代わりに処理にタグを付けるのは、ユーザーが複数の画像にぼかしを入れる場合、画像保存 WorkRequest のすべてに共通するのは、ID ではなくタグになるためです。また、タグは選択することもできます。

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

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

applyBlurSaveImageToFileWorker を作成するときに、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 に、WorkManager.getWorkInfosByTagLiveData を使用して WorkInfo を取得する init ブロックを追加します。

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

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_OUTPUT でタグ付けされた WorkInfo は 1 つのみになります。
  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 を取得し、その 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 を表示するとともにビューモデルの setOutputUri をこの URI を使って呼び出します。

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

[CANCEL WORK] ボタンを追加したので、これを機能させるコードも追加しましょう。WorkManager で処理をキャンセルするには、ID、タグ、一意のチェーン名を使用できます。

今回はキャンセルする処理の指定に一意のチェーン名を使用します。キャンセル対象がチェーン内の特定のステップではなく、すべての処理だからです。

ステップ 1 - 名前を指定して処理をキャンセルする

BlurViewModel に、一意の処理をキャンセルするための新しいメソッド cancelWork() を追加します。この関数の中で workManagercancelUniqueWork を呼び出し、タグ 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. 処理の制約

最後に、WorkManagerConstraints をサポートしていることを忘れてはいけません。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 の UI への表示
  • WorkRequest のキャンセル
  • WorkRequest への制約の追加

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

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

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

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