Σχετικά με αυτό το codelab
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 并验证其功能。