1. 简介
在使用 WorkManager 进行后台工作 代码实验室中,您学习了如何使用 WorkManager 在后台(非主线程)执行工作。在本代码实验室中,您将继续学习 WorkManager 功能,以确保唯一的工作、标记工作、取消工作和工作约束。代码实验室将以学习如何编写自动化测试来验证您的工作程序是否正常运行并返回预期结果结束。您还将学习如何使用 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
,因为如果用户在当前模糊处理完成之前决定模糊另一张图像,则您希望停止当前模糊处理并开始模糊新图像。
您还希望确保如果用户在工作请求已排队时点击 **开始**,则应用会将之前的请求替换为新请求。继续处理之前的请求没有意义,因为应用无论如何都会将其替换为新请求。
在 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>**。 | |
使用 **唯一链名称** 获取工作 | 此函数返回 **LiveData<List<WorkInfo>>**,用于 WorkRequest 的唯一链中的所有工作。 | |
使用 **标记** 获取工作 | 此函数返回 **LiveData<List<WorkInfo>>**,用于标记。 |
WorkInfo
对象包含有关 WorkRequest
当前状态的详细信息,包括
这些方法返回LiveData。LiveData 是一个生命周期感知的可观察数据持有者。我们通过调用.asFlow()
将其转换为 WorkInfo
对象的流。
因为您对最终图像保存的时间感兴趣,所以您向 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 包含值。 - 对于变换规则,如果元素不为空,则选择集合中的第一个项目。否则,返回空值。然后,变换函数将删除空值。
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
更新
- 使用存储库中的
outputWorkInfo
Flow 填充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
,这是状态流的初始值。
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 在其 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 -> {}
}
}
...
对于默认状态,应用显示“开始”按钮。
- 对于
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
状态。对于此状态,应用显示“取消工作”按钮和一个圆形进度指示器。
- 对于
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
状态,该状态发生在图像模糊并保存之后。此时,应用仅显示“开始”按钮。
- 对于其
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)) }
}
}
}
...
运行你的应用
- 运行你的应用并点击“开始”。
- 参考“后台任务检查器”窗口,查看各种状态如何与正在显示的 UI 相对应。
SystemJobService
是负责管理 Worker 执行的组件。
在 worker 运行期间,UI 显示“取消工作”按钮和一个圆形进度指示器。
worker 完成后,UI 会按预期更新为显示“开始”按钮。
5. 显示最终输出
在本节中,将配置应用以显示一个标记为“查看文件”的按钮,只要有模糊图像准备好显示。
创建“查看文件”按钮
“查看文件”按钮仅在 BlurUiState
为 Complete
时显示。
- 打开
ui/BluromaticScreen.kt
文件并导航到BlurActions
可组合项。 - 要在“开始”按钮和“查看文件”按钮之间添加空格,请在
BlurUiState.Complete
块中添加Spacer
可组合项。 - 添加一个新的
FilledTonalButton
可组合项。 - 对于
onClick
参数,传递onSeeFileClick(blurUiState.outputUri)
。 - 为按钮的内容参数添加一个
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。辅助函数创建一个意图并使用它启动一个新的活动来显示保存的图像。
- 打开
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 重置回其初始状态,只显示 **开始** 按钮。
**后台任务检查器** 显示 **已取消** 状态,这是预期的。
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
//...
使用模拟器测试
- 在模拟器上,将 **扩展控件** 窗口中的 **充电级别** 更改为 15% 或更低以模拟低电量场景,**充电器连接** 更改为 **交流充电器**,**电池状态** 更改为 **未充电**。
- 运行应用程序并点击 **开始** 以开始模糊图像。
模拟器的电池电量设置为低电量,因此由于约束条件,WorkManager
不运行 blurWorker
工作请求。它已排队,但会延迟到满足约束条件为止。您可以在 **后台任务检查器** 选项卡中看到此延迟。
- 确认未运行后,缓慢提高电池电量。
电池电量达到约 25% 后,约束条件得到满足,延迟的工作开始运行。此结果显示在 **后台任务检查器** 选项卡中。
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
。如果更改版本,请确保点击 **立即同步** 以使您的项目与更新的 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
创建工作器。测试工作器需要不同的工作构建器。WorkManager API 提供了两个不同的构建器
这两个构建器都允许您测试工作器的业务逻辑。对于CoroutineWorkers
(例如CleanupWorker
、BlurWorker
和SaveImageToFileWorker
),使用TestListenableWorkerBuilder
进行测试,因为它可以处理协程的线程复杂性。
CoroutineWorker
由于使用了协程,因此异步运行。要并行执行工作器,请使用runBlocking
。提供一个空 lambda 函数体以启动,但是您可以使用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
的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()
,则表示访问文件目录没有错误。
现在是时候进行断言以指示工作器已成功。
- 断言工作器的结果为
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
内调用工作器的实现。
- 创建一个
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_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,创建位图,然后将该位图作为文件写入磁盘。如果操作成功,则结果输出为图像 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. 使用后台任务检查器调试 WorkManager
检查工作器
自动化测试是验证工作器功能的好方法。但是,当您尝试调试工作器时,它们提供的实用程序并不多。幸运的是,Android Studio 有一个工具允许您实时可视化、监控和调试工作器。后台任务检查器适用于运行 API 级别 26 或更高版本的模拟器和设备。
在本节中,您将了解**后台任务检查器**提供的一些功能,以检查**Blur-O-Matic**中的工作器。
- 在设备或模拟器上启动**Blur-O-Matic**应用。
- 导航到**查看 > 工具窗口 > 应用检查**。
- 选择**后台任务检查器**选项卡。
- 如有必要,从下拉菜单中选择设备和正在运行的进程。
在示例图像中,进程为com.example.bluromatic
。它可能会自动为您选择进程。如果选择了错误的进程,您可以更改它。
- 单击**工作器**下拉菜单。目前没有运行的工作器,这是有道理的,因为还没有尝试模糊图像。
- 在应用中,选择**更模糊**并单击**启动**。您会立即在**工作器**下拉菜单中看到一些内容。
您现在在**工作器**下拉菜单中看到类似以下内容。
工作器表显示工作器的名称、服务(在本例中为SystemJobService
)、每个工作器的状态和时间戳。在上一步骤的屏幕截图中,请注意BlurWorker
和CleanupWorker
已成功完成其工作。
您也可以使用检查器取消工作。
- 选择一个已排队的 Worker,然后单击工具栏中的**取消选定工作器**。
检查任务详细信息
- 单击**工作器**表中的工作器。
这样做会打开**任务详细信息**窗口。
- 查看**任务详细信息**中显示的信息。
详细信息显示以下类别
- 说明:此部分列出了 Worker 类名称(包含完整限定包名)、分配的标签以及此 worker 的 UUID。
- 执行:此部分显示 worker 的约束条件(如有)、运行频率、状态以及创建和排队此 worker 的类。请记住,BlurWorker 具有一个约束条件,可防止其在电池电量低时执行。当您检查具有约束条件的 Worker 时,这些约束条件会显示在此部分。
- WorkContinuation:此部分显示此 worker 在工作链中的位置。要检查工作链中其他 worker 的详细信息,请点击其 UUID。
- 结果:此部分显示所选 worker 的开始时间、重试次数和输出数据。
图表视图
请记住,**Blur-O-Matic** 中的 worker 是链式连接的。后台任务检查器提供了一个图表视图,可以直观地表示 worker 之间的依赖关系。
在**后台任务检查器**窗口的角落,有两个按钮可在两者之间切换——**显示图表视图**和**显示列表视图**。
- 点击**显示图表视图**
图表视图准确地指示了在**Blur-O-Matic** 应用中实现的 Worker 依赖关系。
- 点击**显示列表视图**以退出图表视图。
其他功能
**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. 恭喜
恭喜!您学习了有关 WorkManger 其他功能的知识,为**Blur-O-Matic** worker 编写了自动化测试,并使用了**后台任务检查器**来检查它们。在此 codelab 中,您学习了:
- 命名唯一的
WorkRequest
链。 - 标记
WorkRequest
。 - 根据
WorkInfo
更新 UI。 - 取消
WorkRequest
。 - 向
WorkRequest
添加约束条件。 - WorkManager 测试 API。
- 如何进行 worker 实现测试。
- 如何测试
CoroutineWorker
。 - 如何手动检查 worker 并验证其功能。