1. 简介
在使用 WorkManager 执行后台工作 Codelab 中,您学习了如何使用 WorkManager 在后台(非主线程)执行工作。在此 Codelab 中,您将继续学习 WorkManager 的功能,以确保工作的唯一性、标记工作、取消工作和工作限制。本 Codelab 的最后,您将学习如何编写自动化测试来验证您的 worker 是否正常运行并返回预期结果。您还将学习如何使用 Android Studio 提供的 后台任务检查器 来检查排队的 worker。
您将构建什么
在此 Codelab 中,您将确保工作的唯一性、标记工作、取消工作以及实现工作限制。然后,您将学习如何为 Blur-O-Matic 应用编写自动化 UI 测试,以验证在使用 WorkManager 执行后台工作 Codelab 中创建的三个 worker 的功能
BlurWorkerCleanupWorkerSaveImageToFileWorker
您将学到什么
您需要什么
- 最新稳定版 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. 确保工作唯一性
既然您已经了解如何链式调用 worker,是时候学习 WorkManager 的另一个强大功能了:唯一工作序列。
有时,您只希望一个工作链同时运行。例如,您可能有一个将本地数据与服务器同步的工作链。您可能希望在开始新的同步之前完成第一个数据同步。为此,您可以使用 beginUniqueWork() 而非 beginWith(),并提供一个唯一的 String 名称。此输入为**整个**工作请求链命名,以便您可以一起引用和查询它们。
您还需要传入一个 ExistingWorkPolicy 对象。此对象会告知 Android OS 如果工作已经存在,会发生什么情况。可能的 ExistingWorkPolicy 值包括 REPLACE、KEEP、APPEND 或 APPEND_OR_REPLACE。
在此应用中,您希望使用 REPLACE,因为如果用户在当前图像处理完成之前决定处理另一张图像,您希望停止当前的图像处理并开始处理新的图像。
您还需要确保,如果用户在工作请求已排队时点击 开始,则应用会用新的请求替换先前的请求。继续处理先前的请求没有意义,因为应用无论如何都会用新的请求替换它。
在 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 获取工作 | 此函数通过 ID 返回特定 WorkRequest 的单个 LiveData<WorkInfo>。 | |
使用 唯一链名称 获取工作 | 此函数返回唯一 WorkRequests 链中所有工作的 LiveData<List<WorkInfo>>。 | |
使用 tag 获取工作 | 此函数返回标记的 LiveData<List<WorkInfo>>。 |
一个 WorkInfo 对象包含有关 WorkRequest 当前状态的详细信息,包括
- 工作是处于
BLOCKED(阻塞)、CANCELLED(已取消)、ENQUEUED(已排队)、FAILED(失败)、RUNNING(运行中)还是SUCCEEDED(成功)状态。 - 如果
WorkRequest已完成,以及工作中的任何输出数据。
这些方法返回 LiveData。LiveData 是一种感知生命周期的可观察数据持有者。我们通过调用 .asFlow() 将其转换为 WorkInfo 对象的 Flow。
由于您关注最终图像保存的时间,您需要为 SaveImageToFileWorker WorkRequest 添加一个标签,以便通过 getWorkInfosByTagLiveData() 方法获取其 WorkInfo。
另一种选择是使用 getWorkInfosForUniqueWorkLiveData() 方法,它返回所有三个 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
您在逻辑中使用 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() 函数将其转换为 Flow。
- 链式调用
.asFlow()函数,将方法转换为 Flow。您转换方法是为了让应用能够使用 Kotlin Flow 而非 LiveData。
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()转换函数保证存在一个值,您可以安全地移除 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 更新
- 使用仓库中的
outputWorkInfoFlow 填充blurUiState变量。
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() 函数需要三个参数
- 对于第一个参数,传入
viewModelScope,它是绑定到 ViewModel 的协程作用域。 - 对于第二个参数,传入
SharingStarted.WhileSubscribed(5_000)。此参数控制共享何时开始和停止。 - 对于第三个参数,传入
BlurUiState.Default,它是状态 Flow 的初始值。
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 从冷的 Flow 转换为热的 StateFlow。
更新 UI
在 ui/BluromaticScreen.kt 文件中,您从 ViewModel 的 blurUiState 变量获取 UI 状态并更新 UI。
一个 when 块控制应用的 UI。此 when 块为 BlurUiState 的三种状态各有一个分支。
UI 在 BlurActions 可组合项内部的 Row 可组合项中更新。完成以下步骤
- 移除
RowComposable 内的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 -> {}
}
}
...
对于默认状态,应用显示 开始 按钮。
- 对于
BlurUiState.Default状态中的onClick参数,传入传递给可组合项的onStartClick变量。 - 对于
stringResourceId参数,传入字符串资源 IDR.string.start。
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 状态。对于此状态,应用显示 取消工作 按钮和圆形进度指示器。
- 对于
BlurUiState.Loading状态中按钮的onClick参数,传入传递给可组合项的onCancelClick变量。 - 对于按钮的
stringResourceId参数,传入字符串资源 IDR.string.cancel_work。
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 状态,它在图像处理并保存后发生。此时,应用仅显示 开始 按钮。
- 对于其
onClick参数在BlurUiState.Complete状态中,传入onStartClick变量。 - 对于其
stringResourceId参数,传入字符串资源 IDR.string.start。
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)) }
}
}
}
...
运行您的应用
- 运行您的应用并点击 开始。
- 参考 后台任务检查器 窗口,查看各种状态与显示的 UI 如何对应。
的 SystemJobService 是负责管理 Worker 执行的组件。
Worker 运行时,UI 会显示 取消工作 按钮和圆形进度指示器。


Worker 完成后,UI 会按预期更新为显示 开始 按钮。


5. 显示最终输出
在本节中,您将配置应用,使其在有可显示的模糊图像时显示一个标有 查看文件 的按钮。
创建 查看文件 按钮
仅当 BlurUiState 为 Complete 时,查看文件 按钮才会显示。
- 打开
ui/BluromaticScreen.kt文件并导航到BlurActions可组合项。 - 要在 开始 按钮和 查看文件 按钮之间添加空间,请在
BlurUiState.Complete块中添加一个Spacer可组合项。 - 添加一个新的
FilledTonalButton可组合项。 - 对于
onClick参数,传入onSeeFileClick(blurUiState.outputUri)。 - 为
Button的 content 参数添加一个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 -> {
// ...
创建查看文件点击事件代码
当用户点击 查看文件 按钮时,其 onClick 处理程序会调用其指定的函数。此函数作为参数传递给 BlurActions() 可组合项的调用。
此函数旨在显示其 URI 中的已保存图像。它调用 showBlurredImage() 辅助函数并传入 URI。辅助函数创建一个 intent 并使用它来启动新 Activity 以显示已保存的图像。
- 打开
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()
)
// ...
运行您的应用
运行您的应用。您现在看到新的、可点击的 查看文件 按钮,它会将您带到已保存的文件


6. 取消工作

之前您添加了 取消工作 按钮,现在您可以添加代码使其执行操作。使用 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() 方法。
- 将
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))
)
// ...
运行您的应用并取消工作
运行您的应用。它编译正常。开始模糊图片,然后点击 取消工作。整个链都被取消了!

取消工作后,由于 WorkInfo.State 为 CANCELLED,因此仅显示 开始 按钮。此更改导致 blurUiState 变量设置为 BlurUiState.Default,从而将 UI 重置回其初始状态并仅显示 开始 按钮。
后台任务检查器 显示状态为 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(未充电)。

- 运行应用并点击 开始 以开始模糊图像。
模拟器的电池电量设置为低,因此 WorkManager 由于约束不会运行 blurWorker 工作请求。它已排队,但会延迟执行,直到满足约束条件。您可以在 后台任务检查器 标签页中看到此延迟。

- 确认它没有运行后,慢慢增加电池电量。
电池电量达到大约 25% 后,约束条件得到满足,延迟的工作开始运行。此结果会显示在 后台任务检查器 标签页中。

8. 编写 Worker 实现的测试
如何测试 WorkManager
为 Worker 编写测试以及使用 WorkManager API 进行测试可能不太直观。Worker 中完成的工作无法直接访问 UI——它严格来说是业务逻辑。通常,您使用本地单元测试测试业务逻辑。但是,您可能还记得在使用 WorkManager 进行后台工作 Codelab 中提到,WorkManager 的运行需要 Android Context。默认情况下,Context 在本地单元测试中不可用。因此,即使没有直接的 UI 元素可供测试,您也必须使用 UI 测试来测试 Worker。
设置依赖项
您需要向项目添加三个 Gradle 依赖项。前两个用于启用 JUnit 和 espresso 进行 UI 测试。第三个依赖项提供工作测试 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的应用程序上下文初始化lateinitcontext 变量。 - 创建一个名为
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 的业务逻辑。对于 CoroutineWorker,例如 CleanupWorker、BlurWorker 和 SaveImageToFileWorker,使用 TestListenableWorkerBuilder 进行测试,因为它处理协程的线程复杂性。
- 鉴于使用了协程,
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 {
}
}
}
- 在
runBlocking的 lambda 主体中,对您在步骤 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 之后的 continuation。因此,它具有相同的输入数据。它从输入数据中获取 URI,创建位图,然后将该位图作为文件写入磁盘。如果操作成功,结果输出是一个图像 URL。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 并点击 开始。您会立即在 Workers 下拉菜单中看到一些内容。
您现在在 Workers 下拉菜单中看到类似以下内容。

Worker 表显示了 Worker 的名称、服务(本例中为 SystemJobService)、每个的状态以及时间戳。在上一张截图示例中,请注意 BlurWorker 和 CleanupWorker 已成功完成其工作。
您还可以使用检查器取消工作。
- 选择一个已排队的 worker,然后点击工具栏中的 Cancel Selected Worker
。
检查任务详细信息
- 点击 Workers 表中的一个 worker。

这样做会弹出 Task Details 窗口。

- 查看 Task Details 中显示的信息。

详细信息显示以下类别
- 描述:此部分列出了 Worker 类名以及完全限定包名、分配的标签和此 worker 的 UUID。
- 执行:此部分显示 worker 的约束(如果有)、运行频率、状态以及创建和排队此 worker 的类。回想一下,BlurWorker 有一个约束,阻止它在电池电量低时执行。当您检查具有约束的 Worker 时,它们会出现在此部分。
- 工作延续:此部分显示此 worker 在工作链中的位置。要检查工作链中另一个 worker 的详细信息,请点击其 UUID。
- 结果:此部分显示选定 worker 的开始时间、重试次数和输出数据。
图视图
回想一下,Blur-O-Matic 中的 worker 是链式的。后台任务检查器提供了一种图视图,可以直观地表示 worker 的依赖关系。
在 后台任务检查器 窗口的角落,有两个按钮可以切换 — Show Graph View(显示图视图)和 Show List View(显示列表视图)。

- 点击 Show Graph View
(显示图视图)。

图视图准确地显示了 Blur-O-Matic 应用中实现的 Worker 依赖关系。
- 点击 Show List View
(显示列表视图)退出图视图。
附加功能
Blur-O-Matic 应用仅实现 Worker 来完成后台任务。但是,您可以在 后台任务检查器 的文档中阅读有关检查其他类型后台工作的可用工具的更多信息。
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. 恭喜
恭喜!您学习了 WorkManager 的其他功能,为 Blur-O-Matic worker 编写了自动化测试,并使用 后台任务检查器 来检查它们。在本 Codelab 中,您学习了
- 命名唯一的
WorkRequest链。 - 标记
WorkRequest。 - 根据
WorkInfo更新 UI。 - 取消
WorkRequest。 - 向
WorkRequest添加约束。 - WorkManager 测试 API。
- 如何进行 worker 实现的测试。
- 如何测试
CoroutineWorker。 - 如何手动检查 worker 并验证其功能。