1. 简介
在使用 WorkManager 处理后台工作 Codelab 中,您学习了如何使用 WorkManager 在后台(而不是在主线程上)执行工作。在此 Codelab 中,您将继续了解用于确保工作具有唯一性、标记工作、取消工作和设置工作约束条件的 WorkManager 功能。在此 Codelab 结束时,您将学习如何编写自动化测试来验证 worker 是否正常运行并返回预期结果。您还将学习如何使用 Android Studio 提供的后台任务检查器检查已加入队列的 worker。
构建内容
在此 Codelab 中,您将确保工作具有唯一性、标记工作、取消工作和实现工作约束条件。然后,您将学习如何为 Blur-O-Matic 应用编写自动化界面测试,以验证在使用 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 OS 如果工作已存在,会发生什么情况。可能的 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. 根据工作状态标记和更新界面
您所做的下一个更改是应用在执行工作时显示的内容。返回的有关已加入队列工作的信息决定了界面需要如何更改。
下表显示了三种不同的方法,您可以通过调用这些方法来获取工作信息:
类型 | 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
决定要在界面中显示哪些可组合项。
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 类型中移除?
,因为它不再需要为可为 null 类型。
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
变量的值。
界面代码使用 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
变量将界面状态信息以 StateFlow
的形式公开。该 Flow 通过调用 stateIn()
函数,从冷 Flow
转换为热 StateFlow
。
更新界面
在 ui/BluromaticScreen.kt
文件中,您可以从 ViewModel
的 blurUiState
变量获取界面状态,并更新界面。
when
代码块用于控制应用的界面。此 when
代码块针对三种 BlurUiState
状态分别有一个分支。
界面会在其 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
参数,请传递字符串资源 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
状态。对于此状态,应用会显示 Cancel Work 按钮和圆形进度指示器。
- 对于处于
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
状态,该状态会在图片进行模糊处理并保存之后发生。目前,应用仅显示 Start 按钮。
- 对于其
BlurUiState.Complete
状态中的onClick
参数,请传递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)) }
}
}
}
...
运行您的应用
- 运行应用,然后点击 Start。
- 查看后台任务检查器窗口,了解各种状态如何与显示的界面相对应。
SystemJobService
是负责管理 worker 执行的组件。
在 worker 运行时,界面会显示 Cancel Work 按钮和圆形进度指示器。
worker 完成操作后,界面就会更新,按预期显示 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”点击事件代码
当用户点击 See File 按钮时,其 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()
)
// ...
运行您的应用
运行应用。您现在会看到新的可点击 See File 按钮,点击该按钮就能打开保存的文件:
6. 取消工作
之前,您已添加 Cancel Work 按钮,现在您可以添加代码,让其执行某项操作。借助 WorkManager,您可以使用 ID、标记和唯一链名称取消工作。
在本例中,您需要使用唯一链名称取消工作,因为您希望取消链中的所有工作,而不仅仅是某个特定步骤。
按名称取消工作
- 打开
data/WorkManagerBluromaticRepository.kt
文件。 - 在
cancelWork()
函数中,调用workManager.cancelUniqueWork()
函数。 - 传入唯一链名称
IMAGE_MANIPULATION_WORK_NAME
,以便调用仅取消具有该名称的已调度工作。
data/WorkManagerBluromaticRepository.kt
override fun cancelWork() {
workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}
遵循关注点分离的设计原则,可组合函数不得直接与代码库交互。可组合函数与 ViewModel 交互,ViewModel 与代码库交互。
这是一种很好的设计原则,原因在于对代码库的更改不需要您更改可组合函数,因为它们不会直接交互。
- 打开
ui/BlurViewModel.kt
文件。 - 创建一个名为
cancelWork()
的新函数来取消工作。 - 在该函数内,对
bluromaticRepository
对象调用cancelWork()
方法。
ui/BlurViewModel.kt
/**
* Call method from repository to cancel any ongoing WorkRequest
* */
fun cancelWork() {
bluromaticRepository.cancelWork()
}
设置“Cancel Work”点击事件
- 打开
ui/BluromaticScreen.kt
文件。 - 转到
BluromaticScreen()
可组合函数。
ui/BluromaticScreen.kt
fun BluromaticScreen(blurViewModel: BlurViewModel = viewModel(factory = BlurViewModel.Factory)) {
val uiState by blurViewModel.blurUiState.collectAsStateWithLifecycle()
val layoutDirection = LocalLayoutDirection.current
Surface(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.padding(
start = WindowInsets.safeDrawing
.asPaddingValues()
.calculateStartPadding(layoutDirection),
end = WindowInsets.safeDrawing
.asPaddingValues()
.calculateEndPadding(layoutDirection)
)
) {
BluromaticScreenContent(
blurUiState = uiState,
blurAmountOptions = blurViewModel.blurAmount,
applyBlur = blurViewModel::applyBlur,
cancelWork = {},
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}
在对 BluromaticScreenContent
可组合项的调用内,您希望 ViewModel 的 cancelWork()
方法在用户点击该按钮时运行。
- 为
cancelWork
参数赋值blurViewModel::cancelWork
。
ui/BluromaticScreen.kt
// ...
BluromaticScreenContent(
blurUiState = uiState,
blurAmountOptions = blurViewModel.blurAmount,
applyBlur = blurViewModel::applyBlur,
cancelWork = blurViewModel::cancelWork,
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.padding_medium))
)
// ...
运行应用并取消工作
运行应用。它编译得很好。开始对照片进行模糊处理,然后点击 Cancel Work。整个链都会被取消!
取消工作后,系统只会显示 Start 按钮,因为 WorkInfo.State
为 CANCELLED
。此更改会导致 blurUiState
变量设置为 BlurUiState.Default
,这会将界面重置为初始状态,并且仅显示 Start 按钮。
后台任务检查器会按预期显示 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 中完成的工作无法直接访问界面,这严格属于业务逻辑。通常,您需要使用本地单元测试来测试业务逻辑。不过,在“使用 WorkManager 处理后台工作”Codelab 中,您可能还记得 WorkManger 需要 Android Context 才能运行。默认情况下,本地单元测试没有 Context。因此,即使没有要测试的直接界面元素,也必须使用界面测试来测试 worker 测试。
设置依赖项
您需要向项目中添加三个 Gradle 依赖项。前两个用于为界面测试启用 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 目录中为您的界面测试创建一个目录。
- 在
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
添加到 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 或更高的模拟器和设备。
在本部分中,您将了解 Background Task Inspector 为检查 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 下拉列表中看到如下内容。
Workers 表显示了 worker 的名称、Service(在本例中为 SystemJobService
)、每个 worker 的状态和时间戳。在上一步的屏幕截图中,请注意 BlurWorker
和 CleanupWorker
已成功完成其工作。
您还可以使用检查器取消工作。
- 选择已加入队列的 worker,然后点击工具栏中的 Cancel Selected Worker 。
检查任务详细信息
- 点击 Workers 表中的 worker。
这样会打开 Task Details 窗口。
- 查看 Task Details 中显示的信息。
详细信息包括以下类别:
- Description:此部分列出了包含完全限定软件包的 worker 类名,以及此 worker 的已分配标记和 UUID。
- Execution:此部分显示了 worker 的约束条件(若有)、运行频率、状态,以及哪个类创建了此 worker 并将其加入队列。回想一下,BlurWorker 有一个约束条件,可以在电池电量不足时阻止它执行。当您检查具有约束条件的 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. 获取解决方案代码
如需下载完成后的 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
更新界面。 - 取消
WorkRequest
。 - 为
WorkRequest
添加约束条件。 - WorkManager 测试 API。
- 如何测试 worker 实现。
- 如何测试
CoroutineWorker
。 - 如何手动检查 worker 并验证其功能。