1. 簡介
在使用 WorkManager 處理背景工作程式碼研究室中,您已瞭解如何使用 WorkManager 在背景 (而非主執行緒) 執行工作。在本程式碼研究室中,您將繼續學習 WorkManager 功能,瞭解如何確保不重複工作、標記工作、取消工作,以及工作限制條件。本程式碼研究室會說明如何編寫自動化測試驗證 worker 運作狀況,並傳回預期結果。此外,您也會瞭解如何使用 Android Studio 提供的背景工作檢查器,檢查佇列中的 worker。
建構項目
在這個程式碼研究室中,您將確保不重複工作、標記工作、取消工作,以及實作工作限制條件。您也會瞭解如何為 Blur-O-Matic 應用程式編寫自動化 UI 測試,驗證在使用 WorkManager 處理背景工作程式碼研究室中建立的三個 worker 功能:
BlurWorker
CleanupWorker
SaveImageToFileWorker
課程內容
軟硬體需求
- 最新的 Android Studio 穩定版
- 完成使用 WorkManager 處理背景工作程式碼研究室課程
- Android 裝置或模擬器
2. 開始設定
下載程式碼
點選下方連結即可下載這個程式碼研究室的所有程式碼:
您也可以視需要從 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. 確保不重複工作
現在您已瞭解如何鏈結 worker,可以開始使用 WorkManager 的另一個強大功能:不重複工作序列。
有時候,您會希望一次只執行一個工作鏈結。舉例來說,假如您有一個用來將本機資料與伺服器保持同步的工作鏈結,您可能會想先完成第一次的資料同步作業,再開始新的同步程序。如要這麼做,請使用 beginUniqueWork()
而非 beginWith()
,並提供不重複的 String
名稱。這個輸入內容會命名整個工作要求鏈結,方便您一併參照及查詢這些要求。
您還需要傳入 ExistingWorkPolicy
物件。這個物件會通知 Android 作業系統如果工作已存在,會發生什麼情況。可能的 ExistingWorkPolicy
值為 REPLACE
、KEEP
、APPEND
、或 APPEND_OR_REPLACE
。
在此應用程式中,建議您使用 REPLACE
,因為若使用者在目前的圖片模糊處理完成前,決定對另一個圖片進行相同處理,設定這個值才能讓您先停止目前作業,開始對新圖片進行模糊處理。
此外,若工作要求排入佇列後,使用者點選「Start」,您也想確保應用程式會執行新的工作要求,而不是先前要求。這是因為,既然應用程式會以新的要求取代先前要求,繼續執行先前要求根本毫無意義。
請在 data/WorkManagerBluromaticRepository.kt
檔案的 applyBlur()
方法中,完成下列步驟:
- 移除對
beginWith()
函式的呼叫,並新增對beginUniqueWork()
函式的呼叫。 - 為
beginUniqueWork()
函式的第一個參數傳入常數IMAGE_MANIPULATION_WORK_NAME
。 - 為第二個參數
existingWorkPolicy
傳入ExistingWorkPolicy.REPLACE
。 - 為第三個參數的
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 需如何變更。
下表顯示了三種方法,您可以呼叫方法來取得工作資訊:
類型 | WorkManager 方法 | 說明 |
使用 ID 取得工作 | 此函式會根據特定 WorkRequest 的 ID,傳回單一 LiveData<WorkInfo>。 | |
使用專屬鏈結名稱取得工作 | 這個函式會為 WorkRequests 專屬鏈結中的所有工作傳回 LiveData<List<WorkInfo>>。 | |
使用標記取得工作 | 這個函式會為標記傳回 LiveData<List<WorkInfo>>。 |
WorkInfo
物件包含 WorkRequest
目前狀態的詳細資料,包括:
這些方法會傳回 LiveData。LiveData 是生命週期感知型可觀測的資料容器,我們會透過呼叫 .asFlow()
將其轉換為 WorkInfo
物件的資料流。
如果您想瞭解圖片成品的儲存時間,可以在 SaveImageToFileWorker
WorkRequest 中新增標記,即可透過 getWorkInfosByTagLiveData()
方法取得 WorkInfo。
另一種方式是使用 getWorkInfosForUniqueWorkLiveData()
方法,此方法會傳回全部三個 WorkRequest (CleanupWorker
、BlurWorker
和 SaveImageToFileWorker
) 的資訊。這個方法的缺點是,您需要額外的程式碼,特別用於找出所需的 SaveImageToFileWorker
資訊。
標記工作要求
標記工作是在 applyBlur()
函式內的 data/WorkManagerBluromaticRepository.kt
檔案中完成。
- 建立
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
建議您使用邏輯中 SaveImageToFileWorker
工作要求的 WorkInfo
資訊,根據 BlurUiState
決定要在 UI 中顯示哪些可組合項。
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()
函式會將其轉換為資料流。
- 鏈結對
.asFlow()
函式的呼叫,以便將該方法轉換為資料流。轉換方法可讓應用程式能夠使用 Kotlin 資料流,而非 LiveData。
data/WorkManagerBluromaticRepository.kt
import androidx.lifecycle.asFlow
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow()
...
- 鏈結對
.mapNotNull()
轉換函式的呼叫,以確保資料流含有值。 - 如果是轉換規則,若元素並非空白,請選取集合中的第一個項目,否則系統會傳回空值。若傳回空值,轉換函式會將其移除。
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()
轉換函式可保證有值存在,因此您可以放心從資料流類型中移除?
,因為其不再需要設為可為空值的類型。
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
資料流存放區發送的 WorkInfo
來設定 blurUiState
變數的值。
UI 程式碼會使用 blurUiState
變數值來判斷要顯示的可組合項。
如要更新 blurUiState
,請完成下列步驟:
- 透過
outputWorkInfo
資料流從存放區填入blurUiState
變數。
ui/BlurViewModel.kt
// ...
// REMOVE
// val blurUiState: StateFlow<BlurUiState> = MutableStateFlow(BlurUiState.Default)
// ADD
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
// ...
- 接下來,您需要根據工作狀態,將資料流中的值對應至
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()
函式來轉換資料流。
呼叫 .stateIn()
函式需要三個引數:
- 針對第一個參數,傳遞
viewModelScope
,也就是與 ViewModel 相連結的協同程式範圍。 - 針對第二個參數,傳遞
SharingStarted.WhileSubscribed(5_000)
。這個參數可控制開始和停止分享的時機。 - 針對第三個參數,傳遞
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
區塊針對三種 BlurUiState
狀態分別有一個分支。
UI 會在 Row
可組合項中的 BlurActions
可組合項更新。請完成下列步驟:
- 移除
Row
可組合項中的Button(onStartClick)
程式碼,將其替換為when
區塊,並使用blurUiState
做為引數。
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 執行作業的元件。
worker 執行期間,UI 會顯示「Cancel Work」按鈕和圓形進度指標。
worker 完成作業後,UI 就會更新,並如預期顯示「Start」按鈕。
5. 顯示最終輸出結果
在本章節中,您將對應用程式進行設定,在經過模糊處理的圖片可供查看時,顯示「See File」按鈕。
建立「See File」按鈕
只有在 BlurUiState
為 Complete
時,才會顯示「See File」按鈕。
- 開啟
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 中設定,取決於工作要求的狀態或 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 {
// ...
- 如果 worker 已完成作業且已填入變數,表示有經過模糊處理的圖片可顯示。
您可以呼叫 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」按鈕時,它的 onClick
處理常式會呼叫其指派的函式。此函式會在對 BlurActions()
可組合項的呼叫中做為引數傳遞。
此函式的目的是顯示其 URI 中儲存的圖片。它會呼叫 showBlurredImage()
輔助函式並傳入 URI。輔助函式會建立意圖,並使用該意圖啟動新活動,用來顯示已儲存的圖片。
- 開啟
ui/BluromaticScreen.kt
檔案。 - 在
BluromaticScreenContent()
函式中,呼叫BlurActions()
可組合函式時,開始為onSeeFileClick
參數建立一個 lambda 函式,該函式會使用名為currentUri
的單一參數。這個方法會保存已儲存圖片的 URI。
ui/BluromaticScreen.kt
// ...
BlurActions(
blurUiState = blurUiState,
onStartClick = { applyBlur(selectedValue) },
onSeeFileClick = { currentUri ->
},
onCancelClick = { cancelWork() },
modifier = Modifier.fillMaxWidth()
)
// ...
- 在 lambda 函式的主體中,呼叫
showBlurredImage()
輔助函式。 - 針對第一個參數,傳遞
context
變數。 - 針對第二個參數,傳遞
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()
}
設定「取消工作」點擊事件
- 開啟
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()
方法。
- 將
blurViewModel::cancelWork
值指派給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 處理背景工作」程式碼研究室曾提到,WorkManger 必須要有 Android Context 才能執行。根據預設,本機單元測試沒有 Context。因此,即使沒有可測試的直接 UI 元素,您仍須使用 UI 測試來進行 worker 測試。
設定依附元件
您必須在專案中新增三個 Gradle 依附元件。前兩個依附元件用來為 UI 測試啟用 JUnit 和 espresso,第三個依附元件則提供工作測試 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 檔案保持同步。
建立測試類別
- 依序點選「app」>「src」目錄,然後為 UI 測試建立目錄。
- 在
androidTest/java
目錄中建立名為WorkerInstrumentationTest
的新 Kotlin 類別。
編寫 CleanupWorker
測試
按照步驟編寫測試,驗證 CleanupWorker
的實作結果。請嘗試按照操作說明自行實作。步驟結束時會提供解決方案。
- 在
WorkerInstrumentationTest.kt
中,建立lateinit
變數來存放Context
的例項。 - 建立加上
@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。測試 worker 需要不同的工作建構工具。WorkManager API 提供兩種不同的建構工具:
這兩種建構工具都能測試 worker 的商業邏輯。請使用 TestListenableWorkerBuilder
對 CoroutineWorkers
(例如 CleanupWorker
、BlurWorker
和 SaveImageToFileWorker
) 進行測試,因為它會處理協同程式的執行緒複雜度。
- 使用協同程式時,
CoroutineWorker
會以非同步方式執行。如要同時執行 worker,請使用runBlocking
。如要開始進行,請先提供一個空白的 lambda 主體,但您可以使用runBlocking
指示 worker 直接執行doWork()
,不要將 worker 排入佇列。
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 {
}
}
}
- 在 lambda 主體的
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()
,表示存取檔案目錄並未發生錯誤。
現在要做出斷言,指出 worker 已順利完成。
- 斷言 worker 的結果為
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_IMAGE_URI to "android.resource://com.example.bluromatic/drawable/android_cupcake"
- 在
blurWorker_doWork_resultSuccessReturnsUri()
函式中建構BlurWorker
,並務必透過setInputData()
方法將您建立的模擬 URI 輸入內容傳遞為工作資料。
與 CleanupWorker
測試類似,您必須在 runBlocking
中呼叫 worker 的實作結果。
- 建立
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)
}
}
- 斷言 worker 已順利完成作業。例如,請查看
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_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
新增至 WorkManager,做為 BlurWorker
結尾的延續。因此,它的輸入資料也相同。從輸入資料中取得 URI,建立點陣圖,然後將點陣圖做為檔案寫入磁碟。如果作業成功,輸出的結果會是圖片網址。SaveImageToFileWorker
的測試與 BlurWorker
測試非常類似,唯一的差別在於輸出資料。
請確認您是否可以自行編寫 SaveImageToFileWorker
的測試!完成後,您可以查看下方的解決方案。請回想您為 BlurWorker
測試採取的做法:
- 建構 worker,傳遞輸入資料。
- 建立
runBlocking
區塊。 - 呼叫 worker 上的
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. 使用背景工作檢查器對 WorkManager 進行偵錯
檢查 worker
自動化測試是驗證 worker 功能的好方法。然而,當您嘗試對 worker 進行偵錯時,自動化測試就不太有用。幸運的是,Android Studio 提供一項工具,可讓您以視覺化方式即時監控 worker 並進行偵錯。背景工作檢查器適用於模擬器和搭載 API 級別 26 以上的裝置。
本節將說明背景工作檢查器的部分功能,可用來檢查 Blur-O-Matic 中的 worker。
- 在裝置或模擬器上啟動 Blur-O-Matic 應用程式。
- 依序前往「View」>「Tool Windows」>「App Inspection」。
- 選取「Background Task Inspector」分頁標籤。
- 如有需要,請從下拉式選單中選取裝置和執行程序。
在範例圖片中,程序為 com.example.bluromatic
。系統可能會自動為您選取程序。如果選取的程序有誤,您可以做出變更。
- 按一下「Workers」下拉式選單。目前並沒有執行中的 worker,這很合理,因為現在並未嘗試將圖片模糊處理。
- 在應用程式中選取「More blurred」,然後按一下「Start」。「Workers」下拉式選單中會隨即顯示某些內容。
您現在會在「Workers」下拉式選單中看到類似如下的結果。
「Worker」資料表會顯示 worker 的名稱、服務 (在此示例中為 SystemJobService
)、每個 worker 的狀態,以及時間戳記。請注意,在前一個步驟的螢幕截圖中,BlurWorker
和 CleanupWorker
已順利完成工作。
您也可以使用檢查器取消工作。
- 選取已加入佇列的 worker,然後按一下工具列中的「Cancel Selected Worker」圖示 。
檢查工作詳細資料
- 在「Workers」資料表中點選任一 worker。
系統會開啟「Task Details」視窗。
- 查看「Task Details」中顯示的資訊。
詳細資料會顯示以下類別:
- Description:這個部分會列出內含完整套件的 worker 類別名稱,以及該 worker 獲得的標記和 UUID。
- Execution:這個部分會顯示 worker 的限制 (如果有)、執行頻率、狀態,以及建立這個 worker 並排入佇列的類別。提醒您,BlurWorker 有一項限制條件,可防止 worker 在電量不足時執行。當您檢查有限制條件的 worker 時,便會在這個部分顯示。
- WorkContinuation:這個部分會顯示此 worker 在工作鏈結中的位置。如要查看工作鏈結中其他 worker 的詳細資料,請按一下其 UUID。
- Results:這個部分會顯示所選 worker 的開始時間、重試次數和輸出資料。
圖表檢視
提醒您,Blur-O-Matic 中的 worker 是鏈結狀態。背景工作檢查器提供圖表檢視模式,能以視覺化方式呈現 worker 的依附元件。
「Background Task Inspector」視窗角落的兩個按鈕可用來切換以下檢視模式:「Show Graph View」和「Show List View」。
- 按一下「Show Graph View」圖示 :
圖表檢視可準確指出在 Blur-O-Matic 應用程式中實作的 worker 依附元件。
- 按一下「Show List View」圖示 即可離開圖表檢視。
其他功能
Blur-O-Matic 應用程式只會實作 worker 來完成背景工作。不過,您可以參閱背景工作檢查器的說明文件,進一步瞭解可用於檢查其他類型的背景作業的工具。
10. 取得解決方案程式碼
完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用這些指令:
$ 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 中開啟。
如要查看本程式碼研究室的解決方案程式碼,請前往 GitHub。
11. 恭喜
恭喜!您已經瞭解 WorkManger 的其他功能、為 Blur-O-Matic 的 worker 編寫自動化測試,並且使用背景工作檢查器檢查 worker。在本程式碼研究室中,您已瞭解以下內容:
- 為不重複的
WorkRequest
鏈結命名。 - 標記
WorkRequest
。 - 根據
WorkInfo
更新 UI。 - 取消
WorkRequest
。 - 在
WorkRequest
中新增限制條件。 - WorkManager 測試 API。
- 如何測試 worker 實作結果。
- 如何測試
CoroutineWorker
。 - 如何手動檢查 worker 並驗證其功能。