高度な WorkManager

この Codelab は、高度な WorkManager のコンセプトについて説明しており、WorkManager を使用したバックグラウンド処理の Codelab で説明した基本事項に基づいています。

WorkManager について理解を深める際に有用な、以下に示すその他のリソースもご覧ください。

作成するアプリの概要

この Codelab では、写真や画像にぼかしを入れて結果をファイルに保存するアプリ、Blur-O-Matic を使用します。WorkManager を使用したバックグラウンド処理の Codelab をすでに完了している場合、同じサンプルアプリを使用します。ここでは、コードに以下の機能を追加します。

  1. カスタム構成
  2. Progress API を使用して、作業の実行中に UI を更新する
  3. ワーカーをテストする

必要なもの

この Codelab を実行するには、最新の Android Studio 安定版が必要です。

LiveDataViewModelView Binding に習熟していることも必要です。これらのクラスを初めて使用する場合は、Android ライフサイクル対応コンポーネント Codelab(特に ViewModel および LiveData 向け)または Room と View Codelab(アーキテクチャ コンポーネントの概要)をご確認ください。

問題が発生した場合

いずれかの時点でこの Codelab に問題が生じた場合、またはコードの最終状態を確認することが必要な場合は、 Blur-o-Matic の最終コードをダウンロードできます。

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

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

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

次のリンクをクリックして、この Codelab の手順に沿って進めるために必要なバージョンのコードをダウンロードします。

作業を始めるためのコードをダウンロードする

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

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

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

アプリを実行します。次の画面が表示されます。プロンプトが表示されたら、写真にアクセスするために必要な権限をアプリに付与します。

画像を選択すると次の画面に進みます。この画面では、ラジオボタンで画像をどの程度ぼかすかを選択できます。[Go] ボタンを押すと、画像がぼかし加工されて保存されます。ぼかし加工の実行中に、作業を終了するための [キャンセル] ボタンがアプリに表示されます。

d6b8946f437ec4e1.png

さきほどダウンロードしたコードには以下が含まれています。

  • WorkerUtils**:** このクラスには、実際にぼかしを入れるコードと、後で Notifications を表示してアプリの速度を落とすために使用するいくつかのメソッドが含まれています。
  • BlurApplication***:** デバッグビルド用に Timber ロギング システムを初期化するための、シンプルな onCreate() メソッドを含むアプリケーション クラス。
  • BlurActivity***:** 画像を表示し、ぼかしの程度を選択するためのラジオボタンを含むアクティビティ。
  • BlurViewModel***:** このビューモデルには、BlurActivity の表示に必要なすべてのデータが格納されています。また、このクラスは WorkManager を使用してバックグラウンド処理を開始するクラスでもあります。
  • Workers/CleanupWorker**:** このワーカーは、一時ファイルが存在する場合に、常にそれを削除します。
  • Workers/BlurWorker***:** このワーカーは、URI によって入力データとして渡された画像をぼかし加工して、一時ファイルの URI を返します。
  • Workers/SaveImageToFileWorker**:** このワーカーは、一時的な画像の URI を入力として受け取り、最終的なファイルの URI を返します。
  • Constants**:** Codelab での作業中に使用する定数を含む静的クラス。
  • SelectImageActivity**:** 画像を選択するための最初のアクティビティ。
  • res/activity_blur.xmlres/activity_select.xml: 各アクティビティのレイアウト ファイル。

***** は、これからコードを書き込むファイルであることを示します。

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

app/build.gradle

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

WorkManager リリースページから work-runtime の最新バージョンを入手し、最新の安定版リリースのバージョンを挿入するか、以下を使用してください。

build.gradle

versions.work = "2.4.0"

[Sync Now] をクリックし、変更された Gradle ファイルとプロジェクトを同期するようにしてください。

このステップでは、カスタム構成をアプリに追加して、デバッグビルド用に WorkManager のロギングレベルを変更します。

ステップ 1 - デフォルトの初期化を無効にする

WorkManager のカスタム構成と初期化のドキュメントで説明されているように、デフォルトで WorkManager ライブラリから自動的にマージされるノードを削除して、AndroidManifest.xml ファイルでデフォルトの初期化を無効にする必要があります。

このノードを削除するには、以下に示すように新しいプロバイダ ノードを AndroidManifest.xml に追加します。

AndroidManifest.xml

<application

...

    <provider
        android:name="androidx.work.impl.WorkManagerInitializer"
        android:authorities="${applicationId}.workmanager-init"
        tools:node="remove" />
</application>

マニフェストにツールの名前空間を追加する必要もあります。これらの変更を実行した完成形のファイルは次のとおりです。

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 -->

<manifest package="com.example.background"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:name=".BlurApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".SelectImageActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name=".BlurActivity" />

        <!-- ADD THE FOLLOWING NODE -->
        <provider
            android:name="androidx.work.impl.WorkManagerInitializer"
            android:authorities="${applicationId}.workmanager-init"
            tools:node="remove" />
    </application>
</manifest>

ステップ 2 - Application クラスに Configuration.Provider を追加する

Application クラスに WorkManager の Configuration.Provider インターフェースを実装すると、オンデマンド初期化を使用できます。アプリが初めて getInstance(context) を使用して WorkManager のインスタンスを取得すると、WorkManager は getWorkManagerConfiguration() から返された構成を使用して自身を初期化します。

BlurApplication.kt

class BlurApplication : Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration =

        Configuration.Builder()
                     .setMinimumLoggingLevel(android.util.Log.DEBUG)
                     .build()
...
}

この変更により、ロギングが DEBUG に設定された状態で WorkManager が実行されます。

より適切なオプションとしては、これと同じ方法で、次のようなコードを使用して、アプリのデバッグビルドのみを目的として WorkManager をセットアップすることが挙げられます。

BlurApplication.kt

class BlurApplication() : Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration {
        return if (BuildConfig.DEBUG) {
            Configuration.Builder()
                    .setMinimumLoggingLevel(android.util.Log.DEBUG)
                    .build()
        } else {
            Configuration.Builder()
                    .setMinimumLoggingLevel(android.util.Log.ERROR)
                    .build()
        }
    }

...
}

完成形の BlurApplication.kt は次のようになります。

BlurApplication.kt

/* Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 */

package com.example.background

import android.app.Application
import androidx.work.Configuration
import timber.log.Timber
import timber.log.Timber.DebugTree

class BlurApplication() : Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration {
        return if (BuildConfig.DEBUG) {
            Configuration.Builder()
                    .setMinimumLoggingLevel(android.util.Log.DEBUG)
                    .build()
        } else {
            Configuration.Builder()
                    .setMinimumLoggingLevel(android.util.Log.ERROR)
                    .build()
        }
    }

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            Timber.plant(DebugTree())
        }
    }
}

ステップ 3 - デバッグモードでアプリを実行する

デバッグビルドがライブラリから受信したすべてのメッセージをログに記録するように、WorkManager が構成されました。

アプリを実行すると、Android Studio の [logcat] タブでログを確認できます。

5f3522812d1bfb18.png

ステップ 4 - 構成可能な要素

パラメータの全一覧については、Configuration.Builder に関する WorkManager のリファレンス ガイドをご覧ください。次の 2 つの追加パラメータに注意してください。

  • WorkerFactory
  • JobId の範囲

WorkerFactory を変更すると、ワーカーのコンストラクタに他のパラメータを追加できます。カスタム WorkerFactory の実装方法について詳しくは、こちらの WorkManager のカスタマイズの記事をご覧ください。アプリで WorkManager と JobScheduler API を併用する場合は、JobId の範囲をカスタマイズして、2 つの API で同じ JobId 範囲が使用されないようにすることをおすすめします。また、v2.4.0 で導入されたこのケースには lint ルールが存在します。

WorkManager v2.3 では、setProgressAsync()(または、CoroutineWorker から使用されている場合は setProgress())を使用してワーカーから取得した進行状況の情報をアプリと共有する機能が追加されています。この情報は WorkInfo で確認でき、UI でユーザーにフィードバックを提供することを目的として使用するためのものです。ワーカーが最終状態(SUCCEEDED、FAILED、または CANCELLED)に達すると、進行状況のデータはキャンセルされます。進行状況を公開してリッスンする方法の詳細については、ワーカーの中間進行状況の監視をご覧ください。

次に、進行状況を示すバーを UI に追加します。これにより、アプリがフォアグラウンドにある場合、ユーザーはぼかし処理の進行状況を確認できます。最終的な結果は次のようになります。

3ca52d773a4d0e8f.png

ステップ 1 - ProgressBar を変更する

レイアウトで ProgressBar を変更するには、android:indeterminate="true" パラメータを削除し、スタイル style="@android:style/Widget.ProgressBar.Horizontal", を追加して、初期値を android:progress="0" に設定する必要があります。さらに、LinearLayout の向きを "vertical" に設定する必要があります。

app/src/main/res/layout/activity_blur.xml

<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <ProgressBar
        android:id="@+id/progress_bar"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:progress="0"
        android:visibility="gone"
        android:layout_gravity="center_horizontal"
        />

    <Button
        android:id="@+id/cancel_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/cancel_work"
        android:visibility="gone"
        />
</LinearLayout>

必要なもう一つの変更は、ProgressBar が初期位置で再開するようにすることです。そのためには、BlurActivity.kt ファイルの showWorkFinished() 関数を更新します。

app/src/main/java/com/example/background/BlurActivity.kt

/**
 * Shows and hides views for when the Activity is done processing an image
 */
private fun showWorkFinished() {
    with(binding) {
        progressBar.visibility = View.GONE
        cancelButton.visibility = View.GONE
        goButton.visibility = View.VISIBLE
        progressBar.progress = 0 // <-- ADD THIS LINE
    }
}

ステップ 2 - ViewModel の進行状況に関する情報を監視する

BlurViewModel ファイルには、チェーンの完了を確認するオブザーバーがすでに存在します。BlurWorker によって送信された進行状況に関する情報を監視するための新しいオブザーバーを追加します。

まず、進行状況をトラッキングするための 2 つの定数を Constants.kt ファイルの末尾に追加します。

app/src/main/java/com/example/background/Constants.kt

// Progress Data Key
const val PROGRESS = "PROGRESS"
const val TAG_PROGRESS = "TAG_PROGRESS"

次ステップでは、このタグを BlurViewModel.kt ファイルの BlurWorkerWorkRequest に追加して、WorkInfo を取得できるようにします。この WorkInfo から、ワーカーの進行状況に関する情報を取得できます。

app/src/main/java/com/example/background/BlurViewModel.kt

// 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())
    }

    blurBuilder.addTag(TAG_PROGRESS) // <-- ADD THIS
    continuation = continuation.then(blurBuilder.build())
}

この WorkRequest をトラッキングする BlurViewModel.kt ファイルに新しい LiveData を追加し、init ブロックで LiveData を初期化します。

app/src/main/java/com/example/background/BlurViewModel.kt

class BlurViewModel(application: Application) : AndroidViewModel(application) {

    internal var imageUri: Uri? = null
    internal var outputUri: Uri? = null
    internal val outputWorkInfoItems: LiveData<List<WorkInfo>>
    internal val progressWorkInfoItems: LiveData<List<WorkInfo>> // <-- ADD THIS
    private val workManager: WorkManager = WorkManager.getInstance(application)

    init {
        // This transformation makes sure that whenever the current work Id changes the WorkStatus
        // the UI is listening to changes
        outputWorkInfoItems = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
        progressWorkInfoItems = workManager.getWorkInfosByTagLiveData(TAG_PROGRESS) // <-- ADD THIS
    }

...
}

ステップ 3 - アクティビティの LiveData を監視する

BlurActivityLiveData を使用して、公開されたすべての進行状況を監視できるようになりました。まず、onCreate() メソッドの末尾に、新しい LiveData オブザーバーを登録します。

app/src/main/java/com/example/background/BlurActivity.kt

// Show work status
viewModel.outputWorkInfoItems.observe(this, outputObserver())

// ADD THE FOLLOWING LINES
// Show work progress
viewModel.progressWorkInfoItems.observe(this, progressObserver())

これでオブザーバーが受信した WorkInfo を確認し、進行状況に関する情報があるかどうかを確認して、それに応じて ProgressBar を更新できるようになりました。

app/src/main/java/com/example/background/BlurActivity.kt

private fun progressObserver(): Observer<List<WorkInfo>> {
    return Observer { listOfWorkInfo ->
        if (listOfWorkInfo.isNullOrEmpty()) {
            return@Observer
        }

        listOfWorkInfo.forEach { workInfo ->
            if (WorkInfo.State.RUNNING == workInfo.state) {
                val progress = workInfo.progress.getInt(PROGRESS, 0)
                binding.progressBar.progress = progress
            }
        }

    }
}

ステップ 4 - BlurWorker の進行状況を公開する

進行状況に関する情報の表示に必要な要素がすべて揃いました。次に、進行状況に関する情報の実際の公開を BlurWorker に追加します。

この例では、一定期間にわたる進行状況に関して情報を公開できるよう、doWork() 関数で長いプロセスをシンプルにシミュレートしています。

ここでの変更は、1 つの遅延を 10 件の小さな遅延に置き換え、イテレーションごとに新しい進行状況を設定することを目的としています。

app/src/main/java/com/example/background/workers/BlurWorker.kt

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

    val resourceUri = inputData.getString(KEY_IMAGE_URI)

    makeStatusNotification("Blurring image", appContext)
    // sleep()
    (0..100 step 10).forEach {
        setProgressAsync(workDataOf(PROGRESS to it))
        sleep()
    }

...
}

元の遅延が 3 秒であったため、係数 10 によって 0.3 秒に短縮するのもよいでしょう。

app/src/main/java/com/example/background/Constants.kt

// const val DELAY_TIME_MILLIS: Long = 3000
const val DELAY_TIME_MILLIS: Long = 300

ステップ 5 - 実行する

この時点でアプリケーションを実行すると、BlurWorker から受信したメッセージが入力された ProgressBar が表示されます。

テストの実行はすべてのアプリにおいて重要なコンポーネントであり、WorkManager などのライブラリを導入する際は、コードを簡単にテストするためのツールを用意することが重要です。

また、WorkManager には、ワーカーを簡単にテストできるヘルパーも用意されています。ワーカーのテストを作成する方法の詳細については、テストに関する WorkManager のドキュメントをご覧ください。

Codelab のこのセクションでは、一般的なユースケースのいくつかを示すワーカークラスのテストを紹介します。

まず、テストを簡単にセットアップできるように、WorkManager をセットアップする TestRule を作成します。

  • 依存関係を追加する
  • WorkManagerTestRuleTestUtils を作成する
  • CleanupWorker 用のテストを作成する
  • BlurWorker 用のテストを作成する

プロジェクトで AndroidTest フォルダをすでに作成していることを前提として、テストで使用する依存関係を追加する必要があります。

app/build.gradle

androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation "androidx.test.ext:junit:1.1.1"
androidTestImplementation "androidx.test:rules:1.2.0"
androidTestImplementation "androidx.test:runner:1.2.0"
androidTestImplementation "androidx.work:work-testing:$versions.work"

これで、TestRule で各要素をまとめて、テストで使用できるようになりました。

app/src/androidTest/java/com/example/background/workers/WorkManagerTestRule.kt

/* Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 */

package com.example.background.workers

import android.content.Context
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.WorkManager
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import org.junit.rules.TestWatcher
import org.junit.runner.Description

class WorkManagerTestRule : TestWatcher() {
    lateinit var targetContext: Context
    lateinit var testContext: Context
    lateinit var configuration: Configuration
    lateinit var workManager: WorkManager

    override fun starting(description: Description?) {
        targetContext = InstrumentationRegistry.getInstrumentation().targetContext
        testContext = InstrumentationRegistry.getInstrumentation().context
        configuration = Configuration.Builder()
                // Set log level to Log.DEBUG to make it easier to debug
                .setMinimumLoggingLevel(Log.DEBUG)
                // Use a SynchronousExecutor here to make it easier to write tests
                .setExecutor(SynchronousExecutor())
                .build()

        // Initialize WorkManager for instrumentation tests.
        WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, configuration)
        workManager = WorkManager.getInstance(targetContext)
    }
}

デバイス(テストを実行するデバイス)にこのテスト画像が必要になるため、テストで使用するヘルパー関数をいくつか作成します。

app/src/androidTest/java/com/example/background/workers/TestUtils.kt

/* Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 */

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import com.example.background.OUTPUT_PATH
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.util.UUID

/**
 * Copy a file from the asset folder in the testContext to the OUTPUT_PATH in the target context.
 * @param testCtx android test context
 * @param targetCtx target context
 * @param filename source asset file
 * @return Uri for temp file
 */
@Throws(Exception::class)
fun copyFileFromTestToTargetCtx(testCtx: Context, targetCtx: Context, filename: String): Uri {
    // Create test image
    val destinationFilename = String.format("blur-test-%s.png", UUID.randomUUID().toString())
    val outputDir = File(targetCtx.filesDir, OUTPUT_PATH)
    if (!outputDir.exists()) {
        outputDir.mkdirs()
    }
    val outputFile = File(outputDir, destinationFilename)

    val bis = BufferedInputStream(testCtx.assets.open(filename))
    val bos = BufferedOutputStream(FileOutputStream(outputFile))
    val buf = ByteArray(1024)
    bis.read(buf)
    do {
        bos.write(buf)
    } while (bis.read(buf) != -1)
    bis.close()
    bos.close()

    return Uri.fromFile(outputFile)
}

/**
 * Check if a file exists in the given context.
 * @param testCtx android test context
 * @param uri for the file
 * @return true if file exist, false if the file does not exist of the Uri is not valid
 */
fun uriFileExists(targetCtx: Context, uri: String?): Boolean {
    if (uri.isNullOrEmpty()) {
        return false
    }

    val resolver = targetCtx.contentResolver

    // Create a bitmap
    try {
        BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(uri)))
    } catch (e: FileNotFoundException) {
        return false
    }
    return true
}

この作業が完了したら、テストの作成を開始できます。

まず、CleanupWorker をテストして、実際にファイルが削除されることを確認します。これを行うには、テスト中のデバイスでテスト画像をコピーし、CleanupWorker の実行後に同じ場所に存在するかどうかを確認します。

app/src/androidTest/java/com/example/background/workers/CleanupWorkerTest.kt

/* Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 */

package com.example.background.workers

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Rule
import org.junit.Test

class CleanupWorkerTest {

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()
    @get:Rule
    var wmRule = WorkManagerTestRule()

    @Test
    fun testCleanupWork() {
        val testUri = copyFileFromTestToTargetCtx(
                wmRule.testContext, wmRule.targetContext, "test_image.png")
        assertThat(uriFileExists(wmRule.targetContext, testUri.toString()), `is`(true))

        // Create request
        val request = OneTimeWorkRequestBuilder<CleanupWorker>().build()

        // Enqueue and wait for result. This also runs the Worker synchronously
        // because we are using a SynchronousExecutor.
        wmRule.workManager.enqueue(request).result.get()
        // Get WorkInfo
        val workInfo = wmRule.workManager.getWorkInfoById(request.id).get()

        // Assert
        assertThat(uriFileExists(wmRule.targetContext, testUri.toString()), `is`(false))
        assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    }
}

このテストは、Android Studio の [Run] メニューから、またはテストクラスの左側にある緑色の長方形を使用することで実行できます。

be955a84b5b00400.png

プロジェクトのルートフォルダからコマンド ./gradlew cAT を使用することで、コマンドラインからテストを実行することもできます。

テストが正しく実行されていることを確認します。

次に、BlurWorker をテストします。このワーカーは、画像の URI を含む入力データが処理されることを想定しています。そのため、2 つのテストをビルドします。1 つ目のテストでは、入力 URI がない場合にワーカーが失敗することを確認し、もう 2 つ目のテストでは入力画像が実際に処理されることを確認します。

app/src/androidTest/java/com/example/background/workers/BlurWorkerTest.kt

/* Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 */

package com.example.background.workers

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.workDataOf
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Rule
import com.example.background.KEY_IMAGE_URI
import org.junit.Test

class BlurWorkerTest {

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()
    @get:Rule
    var wmRule = WorkManagerTestRule()

    @Test
    fun testFailsIfNoInput() {
        // Define input data

        // Create request
        val request = OneTimeWorkRequestBuilder<BlurWorker>().build()

        // Enqueue and wait for result. This also runs the Worker synchronously
        // because we are using a SynchronousExecutor.
        wmRule.workManager.enqueue(request).result.get()
        // Get WorkInfo
        val workInfo = wmRule.workManager.getWorkInfoById(request.id).get()

        // Assert
        assertThat(workInfo.state, `is`(WorkInfo.State.FAILED))
    }

    @Test
    @Throws(Exception::class)
    fun testAppliesBlur() {
        // Define input data
        val inputDataUri = copyFileFromTestToTargetCtx(
                wmRule.testContext,
                wmRule.targetContext,
                "test_image.png")
        val inputData = workDataOf(KEY_IMAGE_URI to inputDataUri.toString())

        // Create request
        val request = OneTimeWorkRequestBuilder<BlurWorker>()
                .setInputData(inputData)
                .build()

        // Enqueue and wait for result. This also runs the Worker synchronously
        // because we are using a SynchronousExecutor.
        wmRule.workManager.enqueue(request).result.get()
        // Get WorkInfo
        val workInfo = wmRule.workManager.getWorkInfoById(request.id).get()
        val outputUri = workInfo.outputData.getString(KEY_IMAGE_URI)

        // Assert
        assertThat(uriFileExists(wmRule.targetContext, outputUri), `is`(true))
        assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    }
}

これらのテストを実行すると、両方とも成功します。

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

  • カスタム構成を作成する
  • ワーカーの進行状況を公開する
  • UI に作業の進行状況を表示する
  • ワーカーのテストを作成する

作業は適切に完了しました。最終状態のコードとすべての変更を確認するには、以下をご覧ください。

最終状態の Blur-o-Matic コード

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

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

WorkManager は、この Codelab で取り扱った機能以外にも多くの機能をサポートしています。詳細については、WorkManager のドキュメントをご覧ください。