1. 简介
在 使用 WorkManager 进行后台工作 codelab 中,您学习了如何使用 WorkManager 在后台(不是在主线程上)执行工作。在本 codelab 中,您将继续学习 WorkManager 功能,以确保唯一工作、标记工作、取消工作和工作约束。codelab 将以学习如何编写自动化测试来验证您的工作程序是否正常运行并返回预期结果而结束。您还将学习如何使用 Android Studio 提供的 **后台任务检查器** 检查排队的 worker。
您将构建什么
在本 codelab 中,您将确保唯一工作、标记工作、取消工作以及实现工作约束。然后,您将学习如何为 **模糊器** 应用编写自动化 UI 测试,以验证在 使用 WorkManager 进行后台工作 codelab 中创建的三个 worker 的功能
BlurWorker
CleanupWorker
SaveImageToFileWorker
您将学到什么
您需要什么
- 最新稳定版本的 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 操作系统如果工作已存在会发生什么情况。可能的 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>**。 | |
使用 **唯一链名称** 获取工作 | 此函数为唯一 WorkRequest 链中的所有工作返回 **LiveData<List<WorkInfo>>**。 | |
使用 **标签** 获取工作 | 此函数返回标签的 **LiveData<List<WorkInfo>>**。 |
WorkInfo
对象包含有关 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
更新
- 使用存储库中的
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
转换为热 StateFlow
。
更新 UI
在 ui/BluromaticScreen.kt
文件中,您可以从 ViewModel
的 blurUiState
变量获取 UI 状态并更新 UI。
一个 when
块控制应用程序的 UI。此 when
块为三个 BlurUiState
状态中的每一个都有一个分支。
UI 在 BlurActions
可组合项的 Row
可组合项内部更新。完成以下步骤
- 删除
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 -> {}
}
}
...
对于默认状态,应用程序显示“开始”按钮。
- 对于
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)
。 - 为
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 {
// ...
- 如果工作完成并且该变量已填充,则表示存在要显示的模糊图像。
您可以通过调用 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 重置回其初始状态,并且仅显示“开始”按钮。
“后台任务检查器”显示“已取消”状态,这是预期的。
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 的后台工作 codelab 中,WorkManger 需要 Android Context 才能运行。默认情况下,本地单元测试中不可用 Context。因此,您必须使用 UI 测试来测试 Worker 测试,即使没有直接的 UI 元素需要测试。
设置依赖项
您需要向项目添加三个 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
创建 Worker。测试 Worker 需要不同的工作构建器。WorkManager API 提供了两个不同的构建器
这两个构建器都允许您测试 Worker 的业务逻辑。对于 CoroutineWorkers
(例如 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
作为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. 使用后台任务检查器调试WorkManager
检查工作器
自动化测试是验证工作器功能的好方法。但是,当您尝试调试工作器时,它们提供的实用程序并不多。幸运的是,Android Studio提供了一个工具,允许您实时可视化、监控和调试工作器。后台任务检查器适用于运行API级别26或更高版本的模拟器和设备。
在本节中,您将学习后台任务检查器提供的一些功能,以检查Blur-O-Matic中的工作器。
- 在设备或模拟器上启动Blur-O-Matic应用。
- 导航到查看 > 工具窗口 > 应用检查。
- 选择后台任务检查器选项卡。
- 如有必要,从下拉菜单中选择设备和正在运行的进程。
在示例图像中,进程为com.example.bluromatic
。它可能会自动为您选择进程。如果选择了错误的进程,您可以更改它。
- 单击工作器下拉菜单。当前没有工作器正在运行,这很合理,因为还没有尝试模糊图像。
- 在应用中,选择更多模糊并点击开始。您会立即在工作器下拉菜单中看到一些内容。
您现在在工作器下拉菜单中看到如下内容。
工作器表显示了工作器的名称、服务(在本例中为SystemJobService
)、每个工作器的状态和时间戳。在上一步骤的屏幕截图中,请注意BlurWorker
和CleanupWorker
已成功完成其工作。
您还可以使用检查器取消工作。
- 选择一个排队的worker并点击取消选定的worker 工具栏中的。
检查任务详细信息
- 单击工作器表中的一个工作器。
这样做会调出任务详细信息窗口。
- 查看任务详细信息中显示的信息。
详细信息显示以下类别
- 描述:此部分列出了工作器类名(包括完整限定包名)以及分配的标签和此工作器的UUID。
- 执行:此部分显示工作器的约束条件(如果有)、运行频率、状态以及哪个类创建并排队了此工作器。回想一下,BlurWorker有一个约束条件,可以防止它在电池电量低时执行。当您检查具有约束条件的工作器时,它们会显示在此部分中。
- WorkContinuation:此部分显示此工作器在工作链中的位置。要检查工作链中另一个工作器的详细信息,请单击其UUID。
- 结果:此部分显示选定工作器的开始时间、重试次数和输出数据。
图形视图
回想一下,Blur-O-Matic中的工作器是链接的。后台任务检查器提供了一个图形视图,可以直观地表示工作器依赖项。
在后台任务检查器窗口的角落,有两个按钮可以在以下两者之间切换——显示图形视图和显示列表视图。
- 点击显示图形视图
图形视图准确地指示了在Blur-O-Matic应用中实现的工作器依赖关系。
- 点击显示列表视图退出图形视图。
其他功能
Blur-O-Matic应用仅实现工作器来完成后台任务。但是,您可以在后台任务检查器的文档中阅读有关检查其他类型后台工作的可用工具的更多信息。
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工作器编写了自动化测试,并使用了后台任务检查器来检查它们。在本codelab中,您学习了
- 命名唯一
WorkRequest
链。 - 标记
WorkRequest
。 - 根据
WorkInfo
更新UI。 - 取消
WorkRequest
。 - 向
WorkRequest
添加约束条件。 - WorkManager测试API。
- 如何处理测试工作器实现。
- 如何测试
CoroutineWorker
。 - 如何手动检查工作器并验证其功能。