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

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 https://github.com/googlecodelabs/android-workmanager

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

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

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

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

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

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

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

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

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

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


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

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

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

  • BlurApplication: このクラスにはアプリのセットアップが含まれています。
  • 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-ktx:$versions.work"
}

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

build.gradle

versions.work = "2.3.4"

新しいバージョンにアップデートした場合は、必ず [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 という名前の新しいクラスを作成します。

ステップ 2 - コンストラクタを追加する

次のようにして、BlurWorker クラスに Worker の依存関係を追加します。

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

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

Worker で画像 res/test.jpg にぼかしを入れます。

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

  1. applicationContext プロパティを呼び出して Context を取得します。これは、この後実行するさまざまなビットマップ操作で必要になります。
  2. 次のようにして、テスト画像から Bitmap を作成します。
val picture = BitmapFactory.decodeResource(
        appContext.resources,
        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 ステートメントを出力します(Timber.e(throwable, "Error applying blur"))。
  7. 続いて Result.failure() を返します。

このステップが完了したコードを以下に示します。

BlurWorker.kt

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.R
import timber.log.Timber

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.test)

            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) {
            Timber.e(throwable, "Error applying blur")
            Result.failure()
        }
    }
}

ステップ 4 - ViewModel に WorkManager を含める

ViewModelWorkManager インスタンスの変数を作成します。

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 の applyBlur() メソッドに追加します。

BlurViewModel.kt

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

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

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


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

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


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

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

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

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

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

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

  1. Data.Builder オブジェクトを作成します。
  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 に渡す

applyBlur メソッドを次のように変更します。

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

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.test)

        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)

        Result.success()
    } catch (throwable: Throwable) {
        Timber.e(throwable)
        Result.failure()
    }
}

ステップ 5 - 一時画像用 URI を出力する

この Worker が完了したら、Result.success() を返します。出力 Data として outputUri を提供し、以降の処理で他の Worker がこの一時画像を簡単に利用できるようにします。この URI は、次の章で Worker のチェーンを作成する際に役立ちます。手順は次のとおりです。

  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 - アプリを実行する

この時点でアプリを実行します。コンパイルされ、同じ動作を行うはずです。

(任意)Android Studio で Device File Explorer を開き、前の手順で行ったのと同様、data/data/com.example.background/files/blur_filter_outputs/<URI> を確認します。

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

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

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

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

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

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

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

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

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

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

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

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

ステップ 2 - Worker を拡張する

次のようにして、CleanupWorker クラスに Worker の依存関係を追加します。

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 androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File
import timber.log.Timber

/**
 * Cleans up temporary files generated during blurring process
 */
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()
                            Timber.i("Deleted $name - $deleted")
                        }
                    }
                }
            }
            Result.success()
        } catch (exception: Exception) {
            Timber.e(exception)
            Result.failure()
        }
    }
} 

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

SaveImageToFileWorker は入力と出力の受け渡しを行います。入力は、キー KEY_IMAGE_URI で格納された String です。出力もまた、キー KEY_IMAGE_URI で格納された String になります。

ファイル操作に関する Codelab ではないので、入力と出力のコードを正しく記述するという 2 つの TODO を含んだコードを以下に示します。これは、前の手順で入出力のために作成したコードとよく似ています(使用するキーはまったく同じです)。

SaveImageToFileWorker.kt

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
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
import timber.log.Timber

/**
 * Saves the image to a permanent file
 */
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 {
                Timber.e("Writing to MediaStore failed")
                Result.failure()
            }
        } catch (exception: Exception) {
            Timber.e(exception)
            Result.failure()
        }
    }
}

ステップ 5 - BlurWorker の通知を変更する

これで、適切なフォルダへの画像の保存を担う Worker のチェーンができました。次に、エミュレータ デバイスでも各 WorkRequest の開始を容易に確認できるよう、処理を遅らせます。BlurWorker の最終版は、次のようになります。

BlurWorker.kt

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

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

    val resourceUri = inputData.getString(KEY_IMAGE_URI)

    makeStatusNotification("Blurring image", appContext)

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

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

        val resolver = appContext.contentResolver

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

        val output = blurBitmap(picture, appContext)

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

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

        Result.success(outputData)
    } catch (throwable: Throwable) {
        Timber.e(throwable)
        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 のチェーンが生成されます。

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

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

ステップ 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()
}

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

チェーンを使えるようになったので、次は 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 つのみになりました。

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

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

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

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

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

種類

WorkManager のメソッド

説明

ID を使用した処理の取得

getWorkInfoByIdLiveData

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

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

getWorkInfosForUniqueWorkLiveData

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

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

getWorkInfosByTagLiveData

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

ここでは、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. LiveData<List<WorkInfo>> の新しい変数 outputWorkInfos を宣言します。
  2. BlurViewModel に、WorkManager.getWorkInfosByTagLiveData を使用して WorkInfo を取得する init ブロックを追加します。

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

BlurViewModel.kt

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

// Add an init block to the BlurViewModel class
init {
    // 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() を呼び出して該当するビューを表示(それ以外は非表示に)します。
  5. 終了済みの場合は、showWorkFinished() を呼び出して該当するビューを表示(それ以外は非表示に)します。

以下にコードを示します。

BlurActivity.kt

// Show work status, added in onCreate()
viewModel.outputWorkInfos.observe(this, workInfosObserver())

// Add this functions
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 - アプリを実行する

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

WorkInfo には getOutputData メソッドもあり、最後に保存された画像を含む出力 Data オブジェクトを取得できます。Kotlin では、言語により生成される合成アクセサー outputData を使用してこのメソッドにアクセスできます。ぼかしを入れた画像が準備できたら、[SEE FILE] ボタンを表示しましょう。

ステップ 1 - SEE FILE ボタンを作成する

activity_blur.xml レイアウトには非表示のボタンがすでに存在します。outputButton にある BlurActivity です。

このボタンに対してクリック リスナーを設定します。これにより、URI を取得して、その URI を表示するアクティビティを開きます。以下のコードを使用できます。

BlurActivity.kt

// Put this inside onCreate()
// 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 as String)
                binding.seeFileButton.visibility = View.VISIBLE
            }
        } else {
            showWorkInProgress()
        }
    }
}

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

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

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

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

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

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 - 処理を実行してキャンセルする

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

最後になりましたが、WorkManagerConstraints をサポートしていることを忘れてはいけません。Blur-O-Matic では、保存時にはデバイスが充電中でなければならないという制約を使用します。

ステップ 1 - 充電の制約を作成して追加する

Constraints オブジェクトを作成するには、Constraints.Builder を使用します。その後、以下に示すように、必要な制約を設定して WorkRequest に追加します。

BlurViewModel.kt

// Put this inside the applyBlur() function
// 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] ウィンドウで充電ステータスを変更できます。

デバイスが充電中でない場合、電源に接続するまで SaveImageToFileWorker, の実行は停止されます。

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

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

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

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

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

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

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