1. 简介
此代码实验室介绍高级 WorkManager 概念。它建立在 使用 WorkManager 进行后台工作 代码实验室中介绍的基础内容之上。
其他可用于熟悉 WorkManager 的资源包括
- WorkManager 指南
- 博客系列:介绍 WorkManager
- ADS 2019 WorkManager 演讲:WorkManager:超越基础
- WorkManager - MAD Skills 系列
您将构建的内容
在此代码实验室中,您将使用 Blur-O-Matic,这是一种模糊照片和图像并将结果保存到文件的应用。如果您已经完成了 使用 WorkManager 进行后台工作 代码实验室,那么它就是一个类似的示例应用(唯一的区别是,此示例应用允许您从照片库中选择自己的图像来模糊)。在这里,您将为代码添加一些功能
- 自定义配置
- 使用 Progress API 在工作执行期间更新 UI
- 测试您的 Worker
您需要什么
要完成此代码实验室,您需要最新版本的 Android Studio 稳定版。
您还应该熟悉 LiveData
、ViewModel
和 View Binding
。如果您不熟悉这些类,请查看 Android 生命周期感知组件代码实验室(专门针对 ViewModel 和 LiveData)或 Room with a View 代码实验室(介绍体系结构组件)。
如果您在任何时候卡住
如果您在任何时候使用此代码实验室时卡住,或者想查看代码的最终状态,您可以
或者,如果您愿意,可以从 GitHub 克隆完成的 WorkManager 代码实验室
$ git clone -b advanced https://github.com/googlecodelabs/android-workmanager
2. 设置
步骤 1 - 下载代码
单击以下链接下载要与本代码实验室一起使用的代码版本
或者,如果您愿意,可以从 GitHub 克隆代码实验室
$ git clone -b advanced_start https://github.com/googlecodelabs/android-workmanager
步骤 2 - 运行应用
运行应用。您应该看到以下屏幕。当提示时,确保授予应用访问照片的权限。
您可以选择图像并转到下一个屏幕,该屏幕上有单选按钮,您可以在其中选择图像模糊的程度。按下 **Go** 按钮将模糊并保存图像。在模糊过程中,应用会显示 **Cancel** 按钮,让您结束工作。
起始代码包含
WorkerUtils
:此类包含实际模糊的代码,以及一些稍后将用于显示Notifications
并减慢应用速度的便捷方法。BlurApplication
:应用类,带有简单的onCreate()
方法,用于为调试版本初始化 Timber 日志系统。BlurActivity
:显示图像的活动,包括用于选择模糊级别的单选按钮。BlurViewModel
:此视图模型存储显示BlurActivity
所需的所有数据。它也将是您使用 WorkManager 启动后台工作的类。Workers/CleanupWorker
:此 Worker 始终会删除存在的临时文件。Workers/BlurWorker
:此 Worker 使用 URI 模糊传递的图像作为输入数据,并返回临时文件的 URI。Workers/SaveImageToFileWorker
:此 Worker 以临时图像的 URI 作为输入,并返回最终文件的 URI。Constants
:一个包含您将在代码实验室中使用的某些常量的静态类。SelectImageActivity
:第一个活动,允许您选择图像。res/activity_blur.xml
和res/activity_select.xml
:每个活动的布局文件。
您将在以下类中进行代码更改:BlurApplication
、BlurActivity
、BlurViewModel
和 BlurWorker
。
3. 将 WorkManager 添加到您的应用
WorkManager
需要以下 gradle 依赖项。这些已包含在文件中
app/build.gradle
dependencies {
implementation "androidx.work:work-runtime-ktx:$versions.work"
}
您应该从 WorkManager 发布页面 获取最新版本的 work-runtime
,并输入最新稳定版本的版本,或使用以下版本
build.gradle
versions.work = "2.7.1"
确保单击 **Sync Now** 以将您的项目与已更改的 Gradle 文件同步。
4. 添加 WorkManager 的自定义配置
在此步骤中,您将向应用添加自定义配置,以修改调试版本的 WorkManager 日志记录级别。
步骤 1 - 禁用默认初始化
如 自定义 WorkManager 配置和初始化 文档中所述,您必须在 AndroidManifest.xml
文件中禁用默认初始化,方法是删除默认情况下由 WorkManager 库自动合并的节点。
要删除此节点,您可以在 AndroidManifest.xml
中添加一个新的提供程序节点,如下所示
AndroidManifest.xml
<application
...
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
</application>
您还需要将工具命名空间添加到清单中。包含这些更改的完整文件将是
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<manifest package="com.example.background"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".BlurApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".SelectImageActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".BlurActivity" />
<!-- ADD THE FOLLOWING NODE -->
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
</application>
</manifest>
步骤 2 - 将 Configuration.Provider 添加到 Application 类
您可以通过在 Application
类中实现 WorkManager 的 Configuration.Provider
接口来使用按需初始化。当您的应用第一次使用 getInstance(context)
获取 WorkManager 的实例时,WorkManager 将使用 getWorkManagerConfiguration()
返回的配置进行初始化。
BlurApplication.kt
class BlurApplication : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration(): Configuration =
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
...
}
通过此更改,WorkManager 将以 DEBUG
设置的日志记录级别运行。
一个更好的选择可能是仅为应用的调试版本以这种方式设置 WorkManager,使用类似于以下内容
BlurApplication.kt
class BlurApplication() : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration(): Configuration {
return if (BuildConfig.DEBUG) {
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
} else {
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.ERROR)
.build()
}
}
...
}
然后完整的 BlurApplication.kt 将变为
BlurApplication.kt
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
package com.example.background
import android.app.Application
import androidx.work.Configuration
import timber.log.Timber
import timber.log.Timber.DebugTree
class BlurApplication() : Application(), Configuration.Provider {
override fun getWorkManagerConfiguration(): Configuration {
return if (BuildConfig.DEBUG) {
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
} else {
Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.ERROR)
.build()
}
}
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(DebugTree())
}
}
}
步骤 3 - 在调试模式下运行应用
WorkManager 现在已配置为以这种方式运行,以便您的调试版本记录来自库的所有消息。
运行应用时,您可以在 Android Studio 的 logcat
选项卡中看到日志
步骤 4 - 您能配置什么?
完整参数列表位于 WorkManager 的 Configuration.Builder
参考指南中。请注意两个额外的参数
WorkerFactory
JobId
范围
修改 WorkerFactory
允许向 Worker 的构造函数添加其他参数。您可以在此 自定义 WorkManager 文章中找到有关如何实现自定义 WorkerFactory 的更多信息。如果您在应用中同时使用 WorkManager 和 JobScheduler
API,建议自定义 JobId
范围,以避免两个 API 使用相同的 JobId
范围。
共享 WorkManager 的进度
WorkManager v2.3 添加了从 Worker 共享进度信息到应用的功能,使用 setProgressAsync()
(或当从 CoroutineWorker
使用时使用 setProgress()
)。可以通过 WorkInfo 观察此信息,旨在用于向用户提供 UI 中的反馈。当 Worker 达到最终状态(SUCCEEDED、FAILED 或 CANCELLED)时,进度数据将被取消。要了解有关如何发布和侦听进度的更多信息,请阅读 观察中间 Worker 进度。
您现在要做的是在 UI 中添加一个进度条,以便如果应用处于前台,用户可以看到模糊的进度。最终结果将类似于
步骤 1 - 修改 ProgressBar
要修改布局中的 ProgressBar,您需要删除 **android:indeterminate="true"
** 参数,添加样式 **style="@android:style/Widget.ProgressBar.Horizontal",
** 并使用 **android:progress="0"
** 设置初始值。您还需要将 LinearLayout
的方向设置为 **"vertical"
**
app/src/main/res/layout/activity_blur.xml
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ProgressBar
android:id="@+id/progress_bar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progress="0"
android:visibility="gone"
android:layout_gravity="center_horizontal"
/>
<Button
android:id="@+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel_work"
android:visibility="gone"
/>
</LinearLayout>
另一个需要的更改是确保 ProgressBar
确实会重新启动到初始位置。您可以在 BlurActivity.kt
文件中更新 showWorkFinished()
函数来执行此操作
app/src/main/java/com/example/background/BlurActivity.kt
/**
* Shows and hides views for when the Activity is done processing an image
*/
private fun showWorkFinished() {
with(binding) {
progressBar.visibility = View.GONE
cancelButton.visibility = View.GONE
goButton.visibility = View.VISIBLE
progressBar.progress = 0 // <-- ADD THIS LINE
}
}
步骤 2 - 在 ViewModel 中观察进度信息
BlurViewModel
文件中已有一个观察器,用于检查链何时完成。添加一个新的观察器,用于观察 BlurWorker
发布的进度。
首先,在 Constants.kt
文件末尾添加几个常量,以跟踪此内容
app/src/main/java/com/example/background/Constants.kt
// Progress Data Key
const val PROGRESS = "PROGRESS"
const val TAG_PROGRESS = "TAG_PROGRESS"
下一步是将此标签添加到BlurWorker
的WorkRequest
中,该标签位于BlurViewModel.kt
文件中,以便您可以检索其WorkInfo
。从该WorkInfo
中,您可以检索工作程序的进度信息。
app/src/main/java/com/example/background/BlurViewModel.kt
// Add WorkRequests to blur the image the number of times requested
for (i in 0 until blurLevel) {
val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
// Input the Uri if this is the first blur operation
// After the first blur operation the input will be the output of previous
// blur operations.
if (i == 0) {
blurBuilder.setInputData(createInputDataForUri())
}
blurBuilder.addTag(TAG_PROGRESS) // <-- ADD THIS
continuation = continuation.then(blurBuilder.build())
}
在BlurViewModel.kt
文件中添加一个新的LiveData
,用于跟踪此WorkRequest
,并在init
块中初始化LiveData
。
app/src/main/java/com/example/background/BlurViewModel.kt
class BlurViewModel(application: Application) : AndroidViewModel(application) {
internal var imageUri: Uri? = null
internal var outputUri: Uri? = null
internal val outputWorkInfoItems: LiveData<List<WorkInfo>>
internal val progressWorkInfoItems: LiveData<List<WorkInfo>> // <-- ADD THIS
private val workManager: WorkManager = WorkManager.getInstance(application)
init {
// This transformation makes sure that whenever the current work Id changes the WorkStatus
// the UI is listening to changes
outputWorkInfoItems = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
progressWorkInfoItems = workManager.getWorkInfosByTagLiveData(TAG_PROGRESS) // <-- ADD THIS
}
...
}
步骤 3 - 在活动中观察 LiveData
现在,您可以在BlurActivity
中使用此LiveData
来观察所有发布的进度。首先在onCreate()
方法的末尾注册一个新的LiveData
观察者。
app/src/main/java/com/example/background/BlurActivity.kt
// Show work status
viewModel.outputWorkInfoItems.observe(this, outputObserver())
// ADD THE FOLLOWING LINES
// Show work progress
viewModel.progressWorkInfoItems.observe(this, progressObserver())
现在,您可以检查观察者中接收到的WorkInfo
,以查看是否存在任何进度信息,并相应地更新ProgressBar
。
app/src/main/java/com/example/background/BlurActivity.kt
private fun progressObserver(): Observer<List<WorkInfo>> {
return Observer { listOfWorkInfo ->
if (listOfWorkInfo.isNullOrEmpty()) {
return@Observer
}
listOfWorkInfo.forEach { workInfo ->
if (WorkInfo.State.RUNNING == workInfo.state) {
val progress = workInfo.progress.getInt(PROGRESS, 0)
binding.progressBar.progress = progress
}
}
}
}
步骤 4 - 从 BlurWorker 发布进度
现在,所有用于显示进度信息所需的组件都已到位。现在该将进度信息的实际发布添加到BlurWorker
中。
此示例只是在我们的doWork()
函数中模拟了一些冗长的过程,以便它可以在定义的时间段内发布进度信息。
这里的更改是将单个延迟替换为 10 个较小的延迟,在每次迭代时设置新的进度。
app/src/main/java/com/example/background/workers/BlurWorker.kt
override fun doWork(): Result {
val appContext = applicationContext
val resourceUri = inputData.getString(KEY_IMAGE_URI)
makeStatusNotification("Blurring image", appContext)
// sleep()
(0..100 step 10).forEach {
setProgressAsync(workDataOf(PROGRESS to it))
sleep()
}
...
}
由于原始延迟为 3 秒,因此最好也将其缩短十倍,降至 0.3 秒。
app/src/main/java/com/example/background/Constants.kt
// const val DELAY_TIME_MILLIS: Long = 3000
const val DELAY_TIME_MILLIS: Long = 300
步骤 5 - 运行
在这一点上运行应用程序,它应该显示一个进度条,其中填充了来自BlurWorker
的消息。
5. 测试 WorkManager
测试是每个应用程序的重要组成部分,在引入 WorkManager 这样的库时,提供轻松测试代码的工具非常重要。
借助 WorkManager,我们还提供了一些帮助程序,可以轻松测试您的工作程序。要详细了解如何为您的工作程序创建测试,您可以参考有关测试的 WorkManager 文档。
在本节代码实验室中,我们将介绍一些工作程序类的测试,展示一些常见用例。
首先,我们希望提供一种简单的方法来设置我们的测试,为此,我们可以创建一个 TestRule 来设置 WorkManager。
- 添加依赖项
- 创建
WorkManagerTestRule
和TestUtils
- 为
CleanupWorker
创建测试 - 为
BlurWorker
创建测试
假设您已经在项目中创建了 AndroidTest 文件夹,我们需要添加一些依赖项来用于我们的测试。
app/build.gradle
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test:rules:1.4.0"
androidTestImplementation "androidx.test:runner:1.4.0"
androidTestImplementation "androidx.work:work-testing:$versions.work"
现在,我们可以开始将这些部分组合在一起,形成一个 TestRule,我们可以在测试中使用它。
app/src/androidTest/java/com/example/background/workers/WorkManagerTestRule.kt
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
package com.example.background.workers
import android.content.Context
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.WorkManager
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import org.junit.rules.TestWatcher
import org.junit.runner.Description
class WorkManagerTestRule : TestWatcher() {
lateinit var targetContext: Context
lateinit var testContext: Context
lateinit var configuration: Configuration
lateinit var workManager: WorkManager
override fun starting(description: Description?) {
targetContext = InstrumentationRegistry.getInstrumentation().targetContext
testContext = InstrumentationRegistry.getInstrumentation().context
configuration = Configuration.Builder()
// Set log level to Log.DEBUG to make it easier to debug
.setMinimumLoggingLevel(Log.DEBUG)
// Use a SynchronousExecutor here to make it easier to write tests
.setExecutor(SynchronousExecutor())
.build()
// Initialize WorkManager for instrumentation tests.
WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, configuration)
workManager = WorkManager.getInstance(targetContext)
}
}
由于我们将在设备上需要此测试图像(测试将在该设备上运行),因此我们可以在测试中创建几个帮助程序函数。
app/src/androidTest/java/com/example/background/workers/TestUtils.kt
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
package com.example.background.workers
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import com.example.background.OUTPUT_PATH
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.util.UUID
/**
* Copy a file from the asset folder in the testContext to the OUTPUT_PATH in the target context.
* @param testCtx android test context
* @param targetCtx target context
* @param filename source asset file
* @return Uri for temp file
*/
@Throws(Exception::class)
fun copyFileFromTestToTargetCtx(testCtx: Context, targetCtx: Context, filename: String): Uri {
// Create test image
val destinationFilename = String.format("blur-test-%s.png", UUID.randomUUID().toString())
val outputDir = File(targetCtx.filesDir, OUTPUT_PATH)
if (!outputDir.exists()) {
outputDir.mkdirs()
}
val outputFile = File(outputDir, destinationFilename)
val bis = BufferedInputStream(testCtx.assets.open(filename))
val bos = BufferedOutputStream(FileOutputStream(outputFile))
val buf = ByteArray(1024)
bis.read(buf)
do {
bos.write(buf)
} while (bis.read(buf) != -1)
bis.close()
bos.close()
return Uri.fromFile(outputFile)
}
/**
* Check if a file exists in the given context.
* @param testCtx android test context
* @param uri for the file
* @return true if file exist, false if the file does not exist of the Uri is not valid
*/
fun uriFileExists(targetCtx: Context, uri: String?): Boolean {
if (uri.isNullOrEmpty()) {
return false
}
val resolver = targetCtx.contentResolver
// Create a bitmap
try {
BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(uri)))
} catch (e: FileNotFoundException) {
return false
}
return true
}
完成这些工作后,我们就可以开始编写测试了。
首先,我们测试CleanupWorker
,以检查它是否真的删除了我们的文件。为此,在测试中将测试图像复制到设备上,然后在执行CleanupWorker
后检查它是否仍在。
app/src/androidTest/java/com/example/background/workers/CleanupWorkerTest.kt
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
package com.example.background.workers
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Rule
import org.junit.Test
class CleanupWorkerTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
var wmRule = WorkManagerTestRule()
@Test
fun testCleanupWork() {
val testUri = copyFileFromTestToTargetCtx(
wmRule.testContext, wmRule.targetContext, "test_image.png")
assertThat(uriFileExists(wmRule.targetContext, testUri.toString()), `is`(true))
// Create request
val request = OneTimeWorkRequestBuilder<CleanupWorker>().build()
// Enqueue and wait for result. This also runs the Worker synchronously
// because we are using a SynchronousExecutor.
wmRule.workManager.enqueue(request).result.get()
// Get WorkInfo
val workInfo = wmRule.workManager.getWorkInfoById(request.id).get()
// Assert
assertThat(uriFileExists(wmRule.targetContext, testUri.toString()), `is`(false))
assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
}
}
现在,您可以从 Android Studio 的“运行”菜单中运行此测试,或者使用测试类左侧的绿色矩形。
您也可以使用命令./gradlew cAT
从项目根文件夹中使用命令行运行测试。
您应该看到您的测试已正确执行。
接下来,我们可以测试 BlurWorker。此工作程序需要一个输入数据,其中包含要处理的图像的 URI,因此我们可以构建几个测试:一个检查如果不存在输入 URI,工作程序是否会失败的测试,以及一个实际处理输入图像的测试。
app/src/androidTest/java/com/example/background/workers/BlurWorkerTest.kt
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
package com.example.background.workers
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.workDataOf
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Rule
import com.example.background.KEY_IMAGE_URI
import org.junit.Test
class BlurWorkerTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
var wmRule = WorkManagerTestRule()
@Test
fun testFailsIfNoInput() {
// Define input data
// Create request
val request = OneTimeWorkRequestBuilder<BlurWorker>().build()
// Enqueue and wait for result. This also runs the Worker synchronously
// because we are using a SynchronousExecutor.
wmRule.workManager.enqueue(request).result.get()
// Get WorkInfo
val workInfo = wmRule.workManager.getWorkInfoById(request.id).get()
// Assert
assertThat(workInfo.state, `is`(WorkInfo.State.FAILED))
}
@Test
@Throws(Exception::class)
fun testAppliesBlur() {
// Define input data
val inputDataUri = copyFileFromTestToTargetCtx(
wmRule.testContext,
wmRule.targetContext,
"test_image.png")
val inputData = workDataOf(KEY_IMAGE_URI to inputDataUri.toString())
// Create request
val request = OneTimeWorkRequestBuilder<BlurWorker>()
.setInputData(inputData)
.build()
// Enqueue and wait for result. This also runs the Worker synchronously
// because we are using a SynchronousExecutor.
wmRule.workManager.enqueue(request).result.get()
// Get WorkInfo
val workInfo = wmRule.workManager.getWorkInfoById(request.id).get()
val outputUri = workInfo.outputData.getString(KEY_IMAGE_URI)
// Assert
assertThat(uriFileExists(wmRule.targetContext, outputUri), `is`(true))
assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
}
}
如果您运行这些测试,它们都应该成功。
6. 恭喜
恭喜!您已完成 Blur-O-Matic 应用程序,在此过程中,您学习了如何
- 创建自定义配置
- 从您的工作程序发布进度
- 在 UI 中显示工作进度
- 为您的工作程序编写测试
出色的“工作”!要查看代码的最终状态和所有更改,请查看
或者,如果您愿意,您可以从 GitHub 克隆 WorkManager 的代码实验室。
$ git clone -b advanced https://github.com/googlecodelabs/android-workmanager
WorkManager 的支持远远超过我们可以在此代码实验室中介绍的内容。要了解更多信息,请访问WorkManager 文档。