1. はじめに
WorkManager によるバックグラウンド処理の Codelab では、WorkManager を使用して(メインスレッドではなく)バックグラウンドで処理を実行する方法を学習しました。この Codelab では、WorkManager が備える処理チェーンを一意にする機能、処理にタグを付ける機能、処理をキャンセルする機能、処理に制約を付ける機能について引き続き学習します。この Codelab では、ワーカーが正常に機能し、期待される結果が返されることを検証する自動テストの作成方法を学習します。また、Android Studio に用意されている Background Task Inspector を使用して、キューに登録されたワーカーを検査する方法についても説明します。
作成するアプリの概要
この Codelab では、処理チェーンを一意にし、処理にタグを付け、処理をキャンセルし、処理の制約を実装します。その後、WorkManager によるバックグラウンド処理の Codelab で作成した 3 つのワーカーの機能を検証する、Blur-O-Matic アプリ用の自動 UI テストの作成方法を学習します。
BlurWorker
CleanupWorker
SaveImageToFileWorker
学習内容
- 処理チェーンを一意にする
- 処理のキャンセル方法
- 処理の制約の定義方法
- Worker の機能を検証する自動テストの作成方法
- Background Task Inspector を使用して、キューに登録されているワーカーを検査する基本的な方法
必要なもの
- Android Studio の最新の安定版
- WorkManager によるバックグラウンド処理の Codelab を完了していること
- Android デバイスまたはエミュレータ
2. 設定方法
コードをダウンロードする
次のリンクをクリックして、この Codelab のコードをすべてダウンロードします。
または、GitHub からコードのクローンを作成することもできます。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git $ cd basic-android-kotlin-compose-training-workmanager $ git checkout intermediate
Android Studio でプロジェクトを開きます。
3.処理チェーンを一意にする
ワーカー チェーンの作成方法を確認したので、今度は WorkManager のもう一つの強力な機能である一意処理シーケンスに取り組みましょう。
実行する処理チェーンを一度に 1 つにしたい場合があります。たとえば、ローカルデータとサーバーとの同期を行う処理チェーンであれば、最初のデータ同期が完了してから 2 つ目を開始するのが望ましいでしょう。そのためには、beginWith()
の代わりに beginUniqueWork()
を使用し、一意の String
の名前を付けます。この入力により、処理リクエストのチェーン全体に名前が付き、まとめて参照やクエリができるようになります。
さらに、ExistingWorkPolicy
オブジェクトを渡す必要もあります。このオブジェクトは、処理がすでに存在する場合に何が起こるかを Android OS に示すものです。指定可能な ExistingWorkPolicy
の値は、REPLACE
、KEEP
、APPEND
、APPEND_OR_REPLACE
です。
このアプリでは REPLACE
を使用します。現在の画像のぼかし処理が終了する前に別の画像にぼかしを入れることが選択された場合、現在の画像の処理を停止し、新しい画像のぼかし処理を開始するためです。
また、処理リクエストがすでにキューに追加されているときに [Start] がクリックされた場合、以前の処理リクエストを新しいリクエストに置き換える必要があります。前のリクエストの処理を継続しても、新しいリクエストに置き換えられるため、意味がありません。
data/WorkManagerBluromaticRepository.kt
ファイルの applyBlur()
メソッド内で、次のように作業します。
beginWith()
関数の呼び出しを削除し、beginUniqueWork()
関数の呼び出しを追加します。beginUniqueWork()
関数の最初のパラメータとして、定数IMAGE_MANIPULATION_WORK_NAME
を渡します。- 2 つ目のパラメータである
existingWorkPolicy
パラメータとして、ExistingWorkPolicy.REPLACE
を渡します。 - 3 つ目のパラメータとして、
CleanupWorker
用のOneTimeWorkRequest
を作成します。
data/WorkManagerBluromaticRepository.kt
import androidx.work.ExistingWorkPolicy
import com.example.bluromatic.IMAGE_MANIPULATION_WORK_NAME
...
// 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 は一度に一つの画像にぼかしを入れるだけになりました。
4. UI にタグ付けし、処理のステータスに基づいて更新する
次に変更するのは、処理の実行時に表示される内容です。キューに登録されている処理に関して返される情報によって、UI の変更方法が決まります。
次の表に、処理情報を取得するために使用できる 3 種類のメソッドを示します。
種類 | WorkManager のメソッド | 説明 |
ID を使用した処理の取得 | ID で指定された WorkRequest の単一の LiveData<WorkInfo> を返します。 | |
一意チェーン名を使用した処理の取得 | 一意の WorkRequest のチェーンのすべての処理の LiveData<List<WorkInfo>> を返します。 | |
タグを使用した処理の取得 | タグの LiveData<List<WorkInfo>> を返します。 |
WorkInfo
オブジェクトは、WorkRequest
の現在のステータスに関する以下の詳細情報を含むオブジェクトです。
上記のメソッドは LiveData を返します。LiveData は、ライフサイクル対応の監視可能なデータホルダーです。ここでは、.asFlow()
を呼び出して、WorkInfo
オブジェクトの Flow に書き換えます。
最終的な画像を保存するタイミングが重要なので、SaveImageToFileWorker
WorkRequest にタグを追加し、getWorkInfosByTagLiveData()
メソッドから WorkInfo を取得できるようにします。
もう一つの選択肢は、getWorkInfosForUniqueWorkLiveData()
メソッドの使用です。このメソッドは 3 つすべての WorkRequest(CleanupWorker
、BlurWorker
、SaveImageToFileWorker
)に関する情報を返します。この方法の欠点は、必要な SaveImageToFileWorker
の情報を見つけるために、追加のコードが必要になることです。
処理リクエストにタグを付ける
処理のタグ付けは、data/WorkManagerBluromaticRepository.kt
ファイルの applyBlur()
関数内で行われます。
SaveImageToFileWorker
処理リクエストを作成するときに、addTag()
メソッドを呼び出してString
定数TAG_OUTPUT
を渡すことで、処理にタグを付けます。
data/WorkManagerBluromaticRepository.kt
import com.example.bluromatic.TAG_OUTPUT
...
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.addTag(TAG_OUTPUT) // <- Add this
.build()
WorkManager ID の代わりにタグを使用して処理にラベルを付けます。これは、ユーザーが複数の画像にぼかしを入れる場合、すべての保存画像 WorkRequest
に同じタグがついていますが、ID は同じでないためです。
WorkInfo を取得する
BlurUiState
に基づいて UI に表示するコンポーザブルを決定する際に、SaveImageToFileWorker
処理リクエストの WorkInfo
情報を使用するというロジックを使用します。
ViewModel は、リポジトリの outputWorkInfo
変数からこの情報を取得して消費します。
これで、SaveImageToFileWorker
処理リクエストにタグを付けたので、以下の手順でその情報を取得できるようになりました。
data/WorkManagerBluromaticRepository.kt
ファイルで、workManager.getWorkInfosByTagLiveData()
メソッドを呼び出してoutputWorkInfo
変数に値を設定します。- このメソッドのパラメータには
TAG_OUTPUT
定数を渡します。
data/WorkManagerBluromaticRepository.kt
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
...
getWorkInfosByTagLiveData()
メソッドを呼び出すと、LiveData が返されます。LiveData は、ライフサイクル対応の監視可能なデータホルダーです。.asFlow()
関数がそれを Flow に変換します。
- このメソッドを Flow に変換するために、
.asFlow()
関数の呼び出しを連結します。LiveData の代わりに Kotlin Flow を使用できるように、このメソッドを変換します。
data/WorkManagerBluromaticRepository.kt
import androidx.lifecycle.asFlow
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow()
...
.mapNotNull()
変換関数の呼び出しを連結して、Flow が値を含むかを確認します。- 変換ルールとして、要素が空でない場合は、コレクションの最初の項目を選択します。それ以外の場合は、null 値を返します。null 値の場合は、その後に変換関数によって削除されます。
data/WorkManagerBluromaticRepository.kt
import kotlinx.coroutines.flow.mapNotNull
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow().mapNotNull {
if (it.isNotEmpty()) it.first() else null
}
...
.mapNotNull()
変換関数が値の存在を保証し、null 値許容型である必要がなくなったので、Flow 型の?
を問題なく削除できます。
data/WorkManagerBluromaticRepository.kt
...
override val outputWorkInfo: Flow<WorkInfo> =
...
- さらに、
BluromaticRepository
インターフェースから?
を削除する必要もあります。
data/BluromaticRepository.kt
...
interface BluromaticRepository {
// val outputWorkInfo: Flow<WorkInfo?>
val outputWorkInfo: Flow<WorkInfo>
...
WorkInfo
の情報がリポジトリから Flow
として出力されます。そして、それを ViewModel
が消費します。
BlurUiState
を更新する
ViewModel
は、outputWorkInfo
Flow のリポジトリから出力された WorkInfo
を使用して、blurUiState
変数の値を設定します。
UI コードでは、blurUiState
変数の値を使用して、表示するコンポーザブルを決定します。
次のようにして blurUiState
を更新します。
blurUiState
変数にリポジトリから出力されたoutputWorkInfo
Flow を設定します。
ui/BlurViewModel.kt
// ...
// REMOVE
// val blurUiState: StateFlow<BlurUiState> = MutableStateFlow(BlurUiState.Default)
// ADD
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
// ...
- 次に、処理の状態に応じて Flow の各値を
BlurUiState
状態にマッピングする必要があります。
処理が完了した場合は、blurUiState
変数を BlurUiState.Complete(outputUri = "")
に設定します。
処理がキャンセルされた場合は、blurUiState
変数を BlurUiState.Default
に設定します。
それ以外の場合は、blurUiState
変数を BlurUiState.Loading
に設定します。
ui/BlurViewModel.kt
import androidx.work.WorkInfo
import kotlinx.coroutines.flow.map
// ...
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
.map { info ->
when {
info.state.isFinished -> {
BlurUiState.Complete(outputUri = "")
}
info.state == WorkInfo.State.CANCELLED -> {
BlurUiState.Default
}
else -> BlurUiState.Loading
}
}
// ...
- StateFlow が必要なので、
.stateIn()
関数の呼び出しを連結して Flow を変換します。
.stateIn()
関数の呼び出しには、以下の 3 つの引数が必要です。
- 最初のパラメータ
viewModelScope
は、ViewModel に紐付けられているコルーチン スコープです。 - 2 つ目のパラメータは
SharingStarted.WhileSubscribed(5_000)
です。このパラメータは、共有の開始と停止のタイミングを制御します。 - 3 つ目のパラメータは、状態フローの初期値である
BlurUiState.Default
です。
ui/BlurViewModel.kt
import kotlinx.coroutines.flow.stateIn
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
// ...
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
.map { info ->
when {
info.state.isFinished -> {
BlurUiState.Complete(outputUri = "")
}
info.state == WorkInfo.State.CANCELLED -> {
BlurUiState.Default
}
else -> BlurUiState.Loading
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = BlurUiState.Default
)
// ...
ViewModel
は blurUiState
変数を通じて UI 状態情報を StateFlow
として公開します。フローは、stateIn()
関数を呼び出して、コールド Flow
をホット StateFlow
に変換します。
UI を更新する
ui/BluromaticScreen.kt
ファイルで、ViewModel
の blurUiState
変数から UI 状態を取得し、UI を更新します。
when
ブロックでアプリの UI を制御します。この when
ブロックには、3 つの BlurUiState
状態に対してそれぞれ分岐があります。
UI では、Row
コンポーザブル内の BlurActions
コンポーザブルが更新されます。次の手順で行います。
Row
コンポーザブル内のButton(onStartClick)
のコードを削除し、それをblurUiState
を引数とするwhen
ブロックに置き換えます。
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
// REMOVE
// Button(
// onClick = onStartClick,
// modifier = Modifier.fillMaxWidth()
// ) {
// Text(stringResource(R.string.start))
// }
// ADD
when (blurUiState) {
}
}
...
アプリを開くとデフォルトの状態になります。この状態は、コードでは BlurUiState.Default
として表されます。
- 次のコードサンプルのように、
when
ブロック内でこの状態の分岐を作成します。
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {}
}
}
...
デフォルトの状態では、アプリに [Start] ボタンが表示されます。
BlurUiState.Default
状態のonClick
パラメータとして、コンポーザブルに渡されるonStartClick
変数を渡します。stringResourceId
パラメータとして、R.string.start
の文字列リソース ID を渡します。
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {
Button(
onClick = onStartClick,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.start))
}
}
}
...
アプリが実際に画像のぼかし処理を行っている状態は、BlurUiState.Loading
状態です。この状態では、[Cancel Work] ボタンと円形の進行状況インジケーターが表示されます。
BlurUiState.Loading
状態のボタンのonClick
パラメータとして、コンポーザブルに渡されるonCancelClick
変数を渡します。- ボタンの
stringResourceId
パラメータとして、R.string.cancel_work
の文字列リソース ID を渡します。
ui/BluromaticScreen.kt
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalButton
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
}
is BlurUiState.Loading -> {
FilledTonalButton(onCancelClick) { Text(stringResource(R.string.cancel_work)) }
CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(R.dimen.padding_small)))
}
}
}
...
最後に設定するのは BlurUiState.Complete
状態です。これは、画像にぼかしを入れて保存すると発生します。この時点では、[Start] ボタンのみが表示されます。
- その
BlurUiState.Complete
状態のonClick
パラメータとして、onStartClick
変数を渡します。 - その
stringResourceId
パラメータとして、R.string.start
の文字列リソース ID を渡します。
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
}
is BlurUiState.Loading -> {
FilledTonalButton(onCancelClick) { Text(stringResource(R.string.cancel_work)) }
CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(R.dimen.padding_small)))
}
is BlurUiState.Complete -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
}
}
}
...
アプリを実行する
- アプリを実行し、[Start] をクリックします。
- [Background Task Inspector] ウィンドウを参照して、各状態と表示される UI との対応を確認します。
SystemJobService
は、Worker の実行を管理するコンポーネントです。
ワーカーの実行中は、UI に [Cancel Work] ボタンと円形の進行状況インジケーターが表示されます。
ワーカーが終了すると UI が更新され、[Start] ボタンが表示されるはずです。
5. 最終出力を表示する
このセクションでは、ぼかしを入れた画像を表示する準備ができたら [See File] ボタンを表示するようアプリを設定します。
[See File] ボタンを作成する
[See File] ボタンを BlurUiState
が Complete
の場合にのみ表示します。
ui/BluromaticScreen.kt
ファイルを開き、BlurActions
コンポーザブルに移動します。- [Start] ボタンと [See File] ボタンの間にスペースを追加するために、
BlurUiState.Complete
ブロック内でSpacer
コンポーザブルを追加します。 - 新しい
FilledTonalButton
コンポーザブルを追加します。 onClick
パラメータとして、onSeeFileClick(blurUiState.outputUri)
を渡します。Button
のコンテンツ パラメータとしてText
コンポーザブルを追加します。Text
のtext
パラメータとして、文字列リソース IDR.string.see_file
を使用します。
ui/BluromaticScreen.kt
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
// ...
is BlurUiState.Complete -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
// Add a spacer and the new button with a "See File" label
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.padding_small)))
FilledTonalButton({ onSeeFileClick(blurUiState.outputUri) })
{ Text(stringResource(R.string.see_file)) }
}
// ...
blurUiState を更新する
BlurUiState
状態は ViewModel で設定され、WorkRequest の状態と、場合によっては bluromaticRepository.outputWorkInfo
変数に依存します。
ui/BlurViewModel.kt
ファイルのmap()
変換の中で、変数outputImageUri
を作成します。- 作成した変数に、
outputData
データ オブジェクトから取得した保存済み画像の URI を設定します。
この文字列は KEY_IMAGE_URI
キーで取得できます。
ui/BlurViewModel.kt
import com.example.bluromatic.KEY_IMAGE_URI
// ...
.map { info ->
val outputImageUri = info.outputData.getString(KEY_IMAGE_URI)
when {
// ...
- ワーカーが終了し、この変数に値が設定されている場合、表示するぼかしを入れた画像が存在します。
この変数にデータが設定されているかどうかを確認するために、outputImageUri.isNullOrEmpty()
を呼び出します。
isFinished
の分岐を更新して、この変数にデータが設定されていることも確認してから、outputImageUri
変数をBlurUiState.Complete
データ オブジェクトに渡します。
ui/BlurViewModel.kt
// ...
.map { info ->
val outputImageUri = info.outputData.getString(KEY_IMAGE_URI)
when {
info.state.isFinished && !outputImageUri.isNullOrEmpty() -> {
BlurUiState.Complete(outputUri = outputImageUri)
}
info.state == WorkInfo.State.CANCELLED -> {
// ...
[See File] のクリック イベント コードを作成する
[See File] ボタンがクリックされると、その onClick
ハンドラが割り当てられた関数を呼び出します。この関数は、BlurActions()
コンポーザブルの呼び出しで引数として渡します。
この関数の目的は、その URI から取得した保存済み画像を表示することです。showBlurredImage()
ヘルパー関数を呼び出し、URI を渡します。ヘルパー関数はインテントを作成し、それを使用して新しいアクティビティを開始して保存済み画像を表示します。
ui/BluromaticScreen.kt
ファイルを開きます。BluromaticScreenContent()
関数のコンポーズ可能な関数BlurActions()
の呼び出しで、currentUri
というパラメータだけを受け取るonSeeFileClick
パラメータのラムダ関数を作成します。この方法で保存済み画像の URI が保存されます。
ui/BluromaticScreen.kt
// ...
BlurActions(
blurUiState = blurUiState,
onStartClick = { applyBlur(selectedValue) },
onSeeFileClick = { currentUri ->
},
onCancelClick = { cancelWork() },
modifier = Modifier.fillMaxWidth()
)
// ...
- ラムダ関数の本体で、
showBlurredImage()
ヘルパー関数を呼び出します。 - 最初のパラメータとして、
context
変数を渡します。 - 2 つ目のパラメータとして
currentUri
変数を渡します。
ui/BluromaticScreen.kt
// ...
BlurActions(
blurUiState = blurUiState,
onStartClick = { applyBlur(selectedValue) },
// New lambda code runs when See File button is clicked
onSeeFileClick = { currentUri ->
showBlurredImage(context, currentUri)
},
onCancelClick = { cancelWork() },
modifier = Modifier.fillMaxWidth()
)
// ...
アプリを実行する
アプリを実行します。クリック可能な新しい [See File] ボタンが表示され、クリックすると保存済みファイルに移動します。
6. 処理をキャンセルする
さきほど [Cancel Work] ボタンを追加したので、コードを追加すればなんらかの処理ができるようになっています。WorkManager で処理をキャンセルするには、ID、タグ、一意チェーン名を使用できます。
今回は、特定の段階だけではなく、チェーン内のすべての処理をキャンセルするため、一意チェーン名で処理をキャンセルします。
名前を指定して処理をキャンセルする
data/WorkManagerBluromaticRepository.kt
ファイルを開きます。cancelWork()
関数で、workManager.cancelUniqueWork()
関数を呼び出します。- 一意チェーン名
IMAGE_MANIPULATION_WORK_NAME
を渡して、その名前のスケジュールされた処理のみを呼び出しでキャンセルするようにします。
data/WorkManagerBluromaticRepository.kt
override fun cancelWork() {
workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}
関心の分離という設計原則に従えば、コンポーズ可能な関数がリポジトリと直接やり取りすることはできません。コンポーズ可能な関数は ViewModel とやり取りし、ViewModel はリポジトリとやり取りします。
この方法は、おすすめの優れた設計原則です。リポジトリに変更を加えても、コンポーズ可能な関数とは直接やり取りしないので、それを変更する必要がないのがその理由です。
ui/BlurViewModel.kt
ファイルを開きます。- 処理をキャンセルする
cancelWork()
という関数を作成します。 - その関数で、
bluromaticRepository
オブジェクトのcancelWork()
メソッドを呼び出します。
ui/BlurViewModel.kt
/**
* Call method from repository to cancel any ongoing WorkRequest
* */
fun cancelWork() {
bluromaticRepository.cancelWork()
}
[Cancel Work] のクリック イベントをセットアップする
ui/BluromaticScreen.kt
ファイルを開きます。- コンポーズ可能な関数
BluromaticScreen()
に移動します。
ui/BluromaticScreen.kt
fun BluromaticScreen(blurViewModel: BlurViewModel = viewModel(factory = BlurViewModel.Factory)) {
val uiState by blurViewModel.blurUiState.collectAsStateWithLifecycle()
val layoutDirection = LocalLayoutDirection.current
Surface(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.padding(
start = WindowInsets.safeDrawing
.asPaddingValues()
.calculateStartPadding(layoutDirection),
end = WindowInsets.safeDrawing
.asPaddingValues()
.calculateEndPadding(layoutDirection)
)
) {
BluromaticScreenContent(
blurUiState = uiState,
blurAmountOptions = blurViewModel.blurAmount,
applyBlur = blurViewModel::applyBlur,
cancelWork = {},
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}
BluromaticScreenContent
コンポーザブルの呼び出しの中で、ボタンがクリックされたときに ViewModel の cancelWork()
メソッドを実行する必要があります。
cancelWork
パラメータに値blurViewModel::cancelWork
を割り当てます。
ui/BluromaticScreen.kt
// ...
BluromaticScreenContent(
blurUiState = uiState,
blurAmountOptions = blurViewModel.blurAmount,
applyBlur = blurViewModel::applyBlur,
cancelWork = blurViewModel::cancelWork,
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.padding_medium))
)
// ...
アプリを実行して処理をキャンセルする
アプリを実行します。正常にコンパイルされるはずです。画像のぼかしを開始したら、[Cancel Work] ボタンを選択します。チェーン全体がキャンセルされます。
処理をキャンセルすると、WorkInfo.State
が CANCELLED
になるため、[Start] ボタンのみが表示されます。この変更により、blurUiState
変数は BlurUiState.Default
に設定され、UI が初期状態にリセットされて [Start] ボタンのみが表示されます。
[Background Task Inspector] には、想定取りに [Cancelled] のステータスが表示されます。
7. 処理の制約
最後に、WorkManager
は Constraints
をサポートしていることを忘れてはいけません。制約とは、WorkRequest が実行される前に満たされている必要がある要件です。
制約の例としては、requiresDeviceIdle()
や requiresStorageNotLow()
が挙げられます。
requiresDeviceIdle()
の制約の場合、値true
が渡されると、デバイスがアイドル状態のときにのみ処理が実行されます。requiresStorageNotLow()
の制約の場合、値true
が渡されると、ストレージに十分な空き容量があるときにのみ処理が実行されます。
Blur-O-Matic では、blurWorker
処理リクエストを実行するにあたって、デバイスのバッテリー残量レベルが低くないという制約を追加します。この制約は、その処理リクエストは遅延され、デバイスのバッテリー残量が低下していないときにのみ実行されることを意味します。
「バッテリー不足ではない」という制約を作成する
data/WorkManagerBluromaticRepository.kt
ファイルで、次の作業を行います。
applyBlur()
メソッドに移動します。continuation
変数を宣言するコードの後で、constraints
という名前の変数を作成します。この変数は、作成する制約のConstraints
オブジェクトを保持します。Constraints.Builder()
関数を呼び出して Constraints オブジェクトのビルダーを作成し、作成した変数に代入します。
data/WorkManagerBluromaticRepository.kt
import androidx.work.Constraints
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
// ...
setRequiresBatteryNotLow()
メソッドを連結して呼び出し、値true
を渡して、デバイスのバッテリー残量が低下していない場合にのみWorkRequest
が実行されるようにします。
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
// ...
.build()
メソッドの呼び出しを連結して、オブジェクトをビルドします。
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.build()
// ...
- 制約オブジェクトを
blurBuilder
処理リクエストに追加するために、.setConstraints()
メソッドの呼び出しを連結し、制約オブジェクトを渡します。
data/WorkManagerBluromaticRepository.kt
// ...
blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))
blurBuilder.setConstraints(constraints) // Add this code
//...
エミュレータでテストする
- エミュレータで [Extended Controls] ウィンドウの [Charge level] を 15% 以下に変更してバッテリー残量の低下をシミュレートし、[Charger connection] を [AC charger] に、[Battery status] を [Not charging] に変更します。
- アプリを実行し、[Start] をクリックして画像のぼかし処理を開始します。
エミュレータのバッテリー残量レベルが「低」に設定されているため、制約により WorkManager
は blurWorker
処理リクエストを実行しません。キューに登録されますが、制約が満たされるまで遅延されます。この遅延は [Background Task Inspector] タブで確認できます。
- 実行されなかったことを確認したら、バッテリーの残量をゆっくりと増やしていきます。
バッテリーの残量が約 25% に達すると制約が満たされ、遅延処理が実行されます。この結果は、[Background Task Inspector] タブに表れます。
8. Worker 実装のテストを作成する
WorkManager のテスト方法
Worker のテストの記述と WorkManager API を使用したテストは、直感に反している場合があります。Worker で行われる処理は UI に直接アクセスできないため、厳密なビジネス ロジックになります。通常は、ローカル単体テストでビジネス ロジックをテストします。しかし、WorkManager によるバックグラウンド処理の Codelab では、WorkManger の実行に Android の Context が必要でした。Context は、デフォルトではローカル単体テストで使用できません。そのため、テストする直接の UI 要素がない場合でも、UI テストで Worker テストをテストする必要があります。
依存関係を設定する
プロジェクトに 3 つの Gradle 依存関係を追加する必要があります。最初の 2 つは、UI テストのための JUnit と espresso を有効にします。3 つ目の依存関係は、処理テスト API を提供します。
app/build.gradle.kts
dependencies {
// Espresso
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
// Junit
androidTestImplementation("androidx.test.ext:junit:1.1.5")
// Work testing
androidTestImplementation("androidx.work:work-testing:2.8.1")
}
work-runtime-ktx
の最新の安定版を使用する必要があります。バージョンを変更したら、必ず [Sync Now] をクリックしてプロジェクトと最新の Gradle ファイルとを同期させます。
テストクラスを作成する
- UI テスト用のディレクトリを [app] > [src] に作成します。
androidTest/java
ディレクトリにWorkerInstrumentationTest
という名前の新しい Kotlin クラスを作成します。
CleanupWorker
テストを作成する
手順に沿ってテストを作成し、CleanupWorker
の実装を検証します。この検証を、手順に沿ってご自身で実装してみてください。解答はこの手順の最後に用意してあります。
WorkerInstrumentationTest.kt
で、Context
のインスタンスを保持するlateinit
変数を作成します。@Before
アノテーションを付けたsetUp()
メソッドを作成します。setUp()
メソッドで、ApplicationProvider
から取得したアプリ コンテキストでlateinit
コンテキスト変数を初期化します。cleanupWorker_doWork_resultSuccess()
というテスト関数を作成します。cleanupWorker_doWork_resultSuccess()
テストで、CleanupWorker
のインスタンスを作成します。
WorkerInstrumentationTest.kt
class WorkerInstrumentationTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun cleanupWorker_doWork_resultSuccess() {
}
}
Blur-O-Matic アプリを作成する際には、OneTimeWorkRequestBuilder
を使用してワーカーを作成します。Worker のテストには、さまざまな処理ビルダーが必要です。WorkManager API には、次の 2 種類のビルダーが用意されています。
どちらのビルダーでも、ワーカーのビジネス ロジックをテストできます。CleanupWorker
、BlurWorker
、SaveImageToFileWorker
などの CoroutineWorkers
は、複雑なコルーチンのスレッドを扱うため、テストには TestListenableWorkerBuilder
を使用します。
- 各
CoroutineWorker
は、コルーチンの使用により非同期に実行されます。ワーカーを並行に実行するために、runBlocking
を使用します。初めは空のラムダ本体を指定し、ワーカーをキューに入れる代わりに、runBlocking
を使用してdoWork()
を直接実行するようワーカーに指示します。
WorkerInstrumentationTest.kt
class WorkerInstrumentationTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun cleanupWorker_doWork_resultSuccess() {
val worker = TestListenableWorkerBuilder<CleanupWorker>(context).build()
runBlocking {
}
}
}
runBlocking
のラムダ本体で、ステップ 5 で作成したCleanupWorker
のインスタンスのdoWork()
を呼び出して、それを値として保存します。
CleanupWorker
が、Blur-O-Matic アプリのファイル構造に保存されているすべての PNG ファイルを削除することを思い出してください。この処理にはファイルの入出力を伴うため、ファイルを削除しようとしたときに例外がスローされる可能性があります。このため、ファイルの削除を try
ブロックで囲んでいます。
CleanupWorker.kt
...
return@withContext 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) {
Log.e(
TAG,
applicationContext.resources.getString(R.string.error_cleaning_file),
exception
)
Result.failure()
}
try
ブロックの最後で Result.success()
を返しています。コードでそれを Result.success()
としている場合、ファイル ディレクトリへのアクセスにエラーはありません。
次に、ワーカーが成功したことを示すアサーションを作成します。
- ワーカーの結果が
ListenableWorker.Result.success()
であることをアサートします。
次の解答コードを確認してください。
WorkerInstrumentationTest.kt
class WorkerInstrumentationTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun cleanupWorker_doWork_resultSuccess() {
val worker = TestListenableWorkerBuilder<CleanupWorker>(context).build()
runBlocking {
val result = worker.doWork()
assertTrue(result is ListenableWorker.Result.Success)
}
}
}
BlurWorker
テストを作成する
次の手順に沿ってテストを作成し、BlurWorker
の実装を検証します。この検証を、手順に沿ってご自身で実装してみてください。解答はこの手順の最後に用意してあります。
WorkerInstrumentationTest.kt
で、blurWorker_doWork_resultSuccessReturnsUri()
というテスト関数を作成します。
BlurWorker
には処理対象の画像が必要です。そのため、BlurWorker
のインスタンスの作成には、処理対象の画像を含む入力データが必要になります。
- テスト関数の外で、モックの URI 入力を作成します。モック URI は、キーと URI 値を含むペアです。この Key-Value ペアとして、次のサンプルコードを使用します。
KEY_IMAGE_URI to "android.resource://com.example.bluromatic/drawable/android_cupcake"
blurWorker_doWork_resultSuccessReturnsUri()
関数内でBlurWorker
を作成し、作成したモック URI 入力をsetInputData()
メソッドで処理データとして渡します。
CleanupWorker
テストと同様に、runBlocking
内のワーカーの実装を呼び出す必要があります。
runBlocking
ブロックを作成します。runBlocking
ブロック内でdoWork()
を呼び出します。
CleanupWorker
とは違い、BlurWorker
にはテストに適した出力データがあります。
- 出力データにアクセスするために、
doWork()
の結果から URI を抽出します。
WorkerInstrumentationTest.kt
@Test
fun blurWorker_doWork_resultSuccessReturnsUri() {
val worker = TestListenableWorkerBuilder<BlurWorker>(context)
.setInputData(workDataOf(mockUriInput))
.build()
runBlocking {
val result = worker.doWork()
val resultUri = result.outputData.getString(KEY_IMAGE_URI)
}
}
- ワーカーが成功したというアサーションを作成します。例として、
BlurWorker
の次のコードをご覧ください。
BlurWorker.kt
val resourceUri = inputData.getString(KEY_IMAGE_URI)
val blurLevel = inputData.getInt(BLUR_LEVEL, 1)
...
val picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri))
)
val output = blurBitmap(picture, blurLevel)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(applicationContext, output)
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
Result.success(outputData)
...
BlurWorker
は、入力データから URI とぼかしレベルを取得し、一時ファイルを作成します。この操作が成功すると、URI を含む Key-Value ペアが返されます。出力の内容が正しいことを確認するには、出力データにキー KEY_IMAGE_URI
が含まれているというアサーションを作成します。
- 出力データに文字列
"file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-"
で始まる URI が含まれているというアサーションを作成します。
- ご自分のテストを次の解答コードに照らして確認してください。
WorkerInstrumentationTest.kt
@Test
fun blurWorker_doWork_resultSuccessReturnsUri() {
val worker = TestListenableWorkerBuilder<BlurWorker>(context)
.setInputData(workDataOf(mockUriInput))
.build()
runBlocking {
val result = worker.doWork()
val resultUri = result.outputData.getString(KEY_IMAGE_URI)
assertTrue(result is ListenableWorker.Result.Success)
assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI))
assertTrue(
resultUri?.startsWith("file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-")
?: false
)
}
}
SaveImageToFileWorker
テストを作成する
その名のとおり、SaveImageToFileWorker
はファイルをディスクに書き込みます。WorkManagerBluromaticRepository
では SaveImageToFileWorker
を BlurWorker
の継続として WorkManager に追加していました。したがって、入力データは同じです。入力データから URI を取得してビットマップを作成し、ファイルとしてディスクに書き込みます。この操作が成功すると、画像の URL が出力されます。SaveImageToFileWorker
のテストは BlurWorker
テストとよく似ていますが、出力データが異なります。
SaveImageToFileWorker
のテストをご自身で作成できるか確認しましょう。完了後に解答を確認できます。BlurWorker
テストの方法を思い出してください。
- 入力データを渡してワーカーを構築します。
runBlocking
ブロックを作成します。- ワーカーの
doWork()
を呼び出します。 - 結果が成功だったことを確認します。
- 正しいキーと値に対して出力を確認します。
解答は次のとおりです。
@Test
fun saveImageToFileWorker_doWork_resultSuccessReturnsUrl() {
val worker = TestListenableWorkerBuilder<SaveImageToFileWorker>(context)
.setInputData(workDataOf(mockUriInput))
.build()
runBlocking {
val result = worker.doWork()
val resultUri = result.outputData.getString(KEY_IMAGE_URI)
assertTrue(result is ListenableWorker.Result.Success)
assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI))
assertTrue(
resultUri?.startsWith("content://media/external/images/media/")
?: false
)
}
}
9. Background Task Inspector を使用して WorkManager をデバッグする
ワーカーを検査する
自動テストは、Worker の機能を検証するのに最適な方法です。しかし、Worker をデバッグしようとしているときには、それほど役に立ちません。幸いなことに、Android Studio には、Worker をリアルタイムで可視化、モニタリング、デバッグできるツールがあります。Background Task Inspector は、API レベル 26 以降を搭載したエミュレータとデバイスで動作します。
このセクションでは、Blur-O-Matic のワーカーの検査に使用する Background Task Inspector の機能をいくつか説明します。
- デバイスまたはエミュレータで Blur-O-Matic アプリを起動します。
- [View] > [Tool Windows] > [App Inspection] に移動します。
- [Background Task Inspector] タブを選択します。
- 必要に応じて、プルダウン メニューからデバイスと実行中のプロセスを選択します。
サンプル画像の例では、プロセスは com.example.bluromatic
です。プロセスは自動的に選択される場合があります。間違ったプロセスが選択された場合には変更できます。
- [Workers] プルダウン メニューをクリックします。この時点で、実行されているワーカーはありません。これは、画像にぼかしを入れるリクエストがなかったためです。
- アプリで [More blurred] を選択し、[Start] をクリックします。すぐに [Workers] プルダウンになんらかの内容が表示されます。
[Workers] プルダウンには次のように表示されます。
Worker の表には、Worker の名前、Service(この場合は SystemJobService
)、それぞれのステータス、タイムスタンプが表示されます。前のステップのスクリーンショットで、BlurWorker
と CleanupWorker
が正常にそれぞれの処理を完了させていることをご覧ください。
インスペクタを使って処理をキャンセルすることもできます。
- キューに入れられているワーカーを選択し、ツールバーの [Cancel Selected Worker ] をクリックします。
タスクの詳細情報を検査する
- [Workers] の表でワーカーをクリックします。
この操作により、[Task Details] ウィンドウが表示されます。
- [Task Details] に表示される情報を確認します。
詳細情報には次のカテゴリが表示されます。
- Description: このセクションには、ワーカークラス名と完全修飾パッケージが一覧表示され、このワーカーに割り当てられたタグと UUID も示されます。
- Execution: このセクションには、ワーカーの制約(存在する場合)、実行頻度、その状態、このワーカーが作成されてキューに入れられたクラスが表示されます。BlurWorker には、電池残量が少ないときには実行できないという制約があることを思い出してください。制約のある Worker を検査すると、このセクションに表示されます。
- WorkContinuation: このセクションには、このワーカーが存在する処理チェーン内の場所が表示されます。処理チェーン内の別のワーカーの詳細を確認するには、UUID をクリックします。
- Results: このセクションには、選択したワーカーの開始時間、再試行回数、出力データが表示されます。
グラフビュー
Blur-O-Matic のワーカーは連結されていることを思い出してください。Background Task Inspector には、ワーカーの依存関係を視覚的に表すグラフビューが用意されています。
[Background Task Inspector] ウィンドウの隅に、[Show Graph View] と [Show List View] との間での切り替えを行う 2 つのボタンがあります。
- [Show Graph View] をクリックします。
グラフビューに、Blur-O-Matic アプリに実装されている Worker の依存関係が正確に表示されます。
- [Show List View] をクリックして、グラフビューを終了します。
その他の機能
Blur-O-Matic アプリは、バックグラウンド タスクを完了するために Worker のみを実装しています。他のタイプのバックグラウンド処理の検査に使用できるツールについては、Background Task Inspector のドキュメントをご覧ください。
10. 解答コードを取得する
この Codelab の完成したコードをダウンロードするには、以下のコマンドを使用します。
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git $ cd basic-android-kotlin-compose-training-workmanager $ git checkout main
または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。
この Codelab の解答コードを確認する場合は、GitHub で表示します。
11. 完了
おめでとうございます。WorkManger のさらなる機能について学び、Blur-O-Matic ワーカーの自動テストを作成し、Background Task Inspector を使用してそれらを調査しました。この Codelab では、以下のことを学びました。
- 一意の
WorkRequest
チェーンの命名 WorkRequest
へのタグ付けWorkInfo
に基づく UI の更新WorkRequest
のキャンセルWorkRequest
への制約の追加- WorkManager テスト API
- ワーカーの実装をテストする方法
CoroutineWorker
のテスト方法- ワーカーを手動で検査して機能を検証する方法