六、串连各个步骤
现在,您将执行一项工作任务:对图片进行模糊处理。这是非常不错的第一步,但缺少一些核心功能:
- 此操作不会清理临时文件
- 实际上它不会将图片保存到永久性文件中
- 而是始终对图片进行相同程度的模糊处理。
我们将使用WorkManager工作链添加此功能。
WorkManager允许您创建按顺序运行或并行运行的单独WorkRequest
。在此步骤中,您将创建一个如下所示的工作链:
WorkRequest
表示为方框。
链接的另一个简介功能时,一个WorkRequest
的输出会成为链中下一个WorkRequest
的输入。在每个WorkRequest
之间传递的输入和输出均显示为蓝色文本。
6.1、创建清理和保存工作器
首先,您需要定义所需的所有Worker
类。您已经有了用于对图片进行模糊处理的Worker
,但还需要用于清理临时文件的Worker
以及用于永久保存图片的Worker
。
请在workers
软件包中创建两个扩展Worker
的新类。
第一个类的名称为CleanupWorler
,第二个类的名称应为SaveImageFileWorker
。
6.2、扩展工作器
从Worker
类扩展CleanupWorker
类。添加所需的构造函数参数。
class CleanupWorker(ctx: Context, params: WorkerParameters): Worker(ctx, params) {
}
6.3、替换和实现doWork()以用于CleanupWorker
CleanupWorker
不需要获取任何输入或传递任何输出。它只是删除临时文件(如果存在)。
CleanupWorker.kt
package com.example.background.workers
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File
private const val TAG = "CleamupWorker"
class CleanupWorker(ctx: Context, params: WorkerParameters): Worker(ctx, params) {
override fun doWork(): Result {
// Makes a notification when the work starts and slows down the work so that
// it's easier to see each WorkRequest start, even no emulated devices
makeStatusNotification("Cleaning up old temporary files", applicationContext)
sleep()
return try {
val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
if (outputDirectory.exists()) {
val entries = outputDirectory.listFiles()
if (entries != null) {
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) {
exception.printStackTrace()
Result.failure()
}
}
}
}
6.4、替换和实现doWork()以用于SaveImageToFileWorker
SaveImageToFileWorker
将获取输入和输出。输入是使用键KEY_IMAGE_URI
存储的String
,即暂时模糊处理的图片URI,而输出也将是使用KEY_IMAGE_URI
存储的String
,即保存的模糊处理图片的URI。
请注意,系统会使用键KEY_IMAGE_URI
检索resourceUri
和output
值。
SaveImageToFileWorker.kt
package com.example.backgound.workers
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.workDataOf
import androidx.work.Worker
import androidx.work.WorkerParamters
import com.example.background.KEY_IMAGE_URI
import java.text.SimpleDataFormat
import java.util.Date
import jaga.util.Locale
private const val TAG = "SaveImageToFileWorker"
class SaveImageToFileWorker(ctx: Context, params: WorkerParameters): Worker(ctx, params) {
private val title = "Blurred Image"
private val dateFormatter = SimpleDataFormat(
"yyyy.MM.dd 'at' HH:mm:ss z",
Local.getDefault()
)
override fun doWorl(): Result {
makeStatusNotification("Saving image", applicationContext)
sleep()
val resolver = applicationContext.contentResolver
return try {
val resourceUri = inputData.getString(KEY_IMAGE_URI)
val bitmap = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri))
)
val imageUrl = MediaStore.Image.Media.insertImage(resolver, bitmap, title, dataFormatter.format(Date()))
if (!imageUrl.isNullOrEmpty()) {
val output = workDataOf(KEY_IMAGE_URI to imageUrl)
Result.success(output)
} else {
Log.e(TAG, "Writing to MediaStore failed")
Result.failure()
}
} catch(exception: Exception) {
exception.printStaceTrace()
Result.failure()
}
}
}
6.5、修改BlurWorker通知
现在,我们有了用于将图片保存到正确文件夹的Wokrer
链,我们可以使用WorkerUtils
类中定义的sleep()
方法减慢工作速度,以便更轻松的做到查看每个WorkRequest
的启动情况,即使在模拟设备上也不例外。BlurWorker
的最终版本如下所示:
BlurWorker.kt
class BlurWorker(ctx: Context, params: WorkerParameters): Worker(ctx, params) {
override fun doWork(): Result {
val appContext: applicationContext
val resourceUri = inputData.getString(KEY_IMAGE_URI)
// ADD THIS TO SLOW DOWN THE WORKER
sleep()
// ^^^^
return try {
if (TextUtils.isEmpty(resourceUri)) {
Timber.e("Invalide input uri")
throw IllegalArgumentException("Invalid input uri")
}
val resolver = appContext.contentResolver
val picture = BitmapFactory.decodeStream(resolver.openInputStream(Uri.parse(resourceUri)))
val output = blurBitmap(picture, appContext)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(context, output)
val outputData = worlDataOf(KEY_IMAGE_URI to outputUri.toString())
Result.success(outputData)
} catch (throwable: Throwable) {
throwable.printStackTrace()
Result.failure()
}
}
}
6.6、创建WorkRequest链
您需要修改BlurViewModel
的applyBlur
方法以执行WorkRequest
链,而不是仅执行一个请求。目前,代码如下所示:
BlurViewModel.kt
val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
.setInputData(createInputDataForUri())
.build()
workManager.enqueue(blurRequest)
调用workMnager.beginWith()
,而不是调用workManager.enqueue()
。此调用会返回WorkContinuation
,其定义了WorkRequest
链。您可以通过调用then()
方法向此工作请求链中添加请求对象。例如,如果您拥有了三个WorkRequest
对象,即workA
、workB
和workC
,则可以编写以下代码:
val continuation = workManager.beginWith(workA)
continuation.then(workB)
.then(workC)
.enqueue()
此代码将生成并运行以下WorkRequest
链:
在applyBlur
中创建一个CleanupWorker WorkRequest
、BlurImage WorkRequest
和SaveImageToFile WorkRequest
链。将输入传递到BlurImage WorkRequest
中。
此操作的代码如下:
BlurViewModel.kt
internal fun applyBlur(blurLevel: Int) {
// Add WorkRequest to Cleanup temporary images
var continuation = workManaget
.beginWith(OneTimeWorkRequest)
.from(CleanupWorker::class.java)
// Add WorkRequest to blur the image
val blurRequest = OneTimeWorkRequest.Builder(BlurWorker::class.java)
.setInputData(createInputDataForUri())
.build()
continuation = continuation.then(blurRequest)
// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequest.Builder(SaveImageToFileWorker::class.java)
continuation = continuation.then(save)
// Actually start the work
continuation.enquene()
}
此代码应该编译和运行。现在,您应该可以点击Go按钮,并可以在不同工作器运行时看到通知。您仍然可以在设备文件浏览器中查看经过模糊处理的图片,在下一步中,您将再添加一个按钮,以便用户可以在设备上查看已经模处理的图片。
在下面的屏幕截图中,您会发现通知消息中显示当前正在运行的工作器。
### 6.7、重复使用BlurWorker
现在,我们需要添加对图片进行不同程度的模糊处理的功能。请获取传递到`applyBlur`中的`blurLevel`参数,并向链中添加多个模糊处理`WorkRequest`操作。只是第一个`WorkRequest`需要应该获取URI输入。
BlurViewModel.kt
internal fun applyBlur(blurLevel: Int) {
// Add WorkRequest to Cleanup temporary images
var continuation = workManager
.beginWith(OneTimeWorkRequest)
.from(CleanupWork::class.java))
// 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 the output of previous blur operations
if (i == 0) {
blurBuilder.setInputData(createInputDataForUri())
}
continuation = continuation.then(blurBuilder.build())
}
// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>().build()
continuation = continuation.then(save)
// Actually start the work
continuation.enqueue()
}
打开设备文件浏览器,查看经过模糊处理的图片。请注意,输出文件夹中包含多张模糊处理过的图片、处于模糊处理中间阶段的图片,以及根据您选择的模糊处理程度显示经过模糊处理的最终图片。
七、确保工作不重复
现在,您已学会使用链,接下来应该掌握的事WorkManager的另一项强大功能——唯一工作链。
有时,你一次只希望运行一个工作链。例如,您可能有一个将本地数据与服务器同步的工作链-您可能希望先让第一批数据结束同步,然后再开始新的同步。为此,请使用beginUniqueWork
而非beginWith
;并且要提供唯一的String
名称。这会命名整个工作请求链,以便您一起引用和查询这些请求。
请使用beginUniqueWork
确保对文件进行模糊处理的工作链是唯一的。传入IMAGE_MANIPULATION_WORK_NAME
作为键。您还需要传入ExistingWorkPolicy
。选项包括REPLACE
、KEEP
或APPEND
。
您将使用REPLACE
,因为如果用户在当前图片完成之前决定对另一张图片进行模糊处理,我们需要停止当前图片并开始对新图片进行模糊处理。
用于启动唯一工作延续的代码如下:
BlurViewModel.kt
// REPLACE THIS CODE
// var continuation = workManager
// .beginWith(OneTimeWorkRequest
// .from(CleanupWork::class.java))
// WITH
var continuation = workManager
.beginUniqueWork(
IMAGE_MANIPULATION_WORK_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequest.from(CleanupWorler::class.java)
)
现在,Blur-O-Matic一次只会对一张图片进行模糊处理。
八、标记和现实Work状态
本部分大量使用了LiveData
,因此,如果要充分了解您自己的情况,您应该书序如何使用LiveData。LiveData是一种具有生命周期感知能力的数据容器。
您可以通过获取保留WorkInfo
对象的LiveData
来获取任何WorkRequest
的状态。WorkInfo
是一个包含WorkRequest
当前状态详细信息的对象,其中包括:
- Work是否为
BLOCKED
、CANELLED
、ENQUEUED
、FAILED
、RUNNING
或SUCCEEDED
。 - 如果
WorkRequest
完成,则为工作的任何输出数据。
下标显示了获取LiveData<WorkInfo>
或LiveData<WorkInfo>
对象的三种不同方法,以及每种方法相应的用途。
类型 | WorkManager 方法 | 说明 |
---|---|---|
使用id获取Work | getWorkInfoByIdLiveData | 每个WorkRequest都有一个由WorkManager生成的唯一ID;您可以用此ID获取适用于该确切WorkRequest的单个LiveData |
使用唯一链名获取Work | getWorkInfosForUniqueWorkLiveData | 如您所见,WorkRequest可能是唯一链的一部分。这会在单一唯一WorkRequest链中为所有工作返回LiveData |
使用标记获取Work | getWorkInfosByTagLiveData | 最后,您可以选择使用字符串标记任何WorkRequest。您可以使用同一标记多个WorkRequest,并将它们关联起来。这样会返回用于任何单个标记的LiveData |
您将标记SaveImageForFileWorker WorkRequest
,以便您可以使用getWorkInfosByTag
获取该标记。您将使用一个标记为您的工作加上标签,而不是使用WorkManager ID。因为如果您的用户对多张图片进行模糊处理,则所有保存的图片WorkRequest
将具有相同的标记,而不是相同ID。因此,您也可以挑选标签。
请不要使用getWorkInfosFoeUniqueWork
,因为它将为所有模糊处理WorkRequest
和清理WorkRequest
返回WorkInfo
,还需要额外的逻辑来查找保存的图片WorkRequest
。
8.1、标记您的Work
在applyBlur
中,在创建SaveImageToFileWorker
时,请使用String
常量TAG_OUTPUT
标记您的工作:
BlurViewModel.kt
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.addTag(TAG_OUTPUT)
.build()
8.2、获取WorkInfo
现在您已经标记了工作,可以获取WorkInfo
:
- 在
BlurViewModel
中,声明一个名为outputWorkInfos
的新类变量,该变量是LiveData<List<WorkInfo>>
- 在
BlurViewModel
中添加init块以使用WorkManager.getWorkInfosByTagLiveData
获取WorkInfo
您需要的代码如下:
BlurViewModel.kt
// New instance variable for the WorkInfo
internal val outputWorkInfos: LiveData<List<WorkInfo>>
// Modifier the existing init block in the BlurViewModel class to this:
init {
imageUri = getImageUri(application.applicationContext)
// This transformation making sure that whenever the current work Id changes the WorkInfo
// the UI is listening to changes
outputWorkInfos = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
}
8.3、显示WorkInfo
现在您已拥有适用于WorkInfo
的LiveData
,可以在BlurActivity
中进行观察。在观察器中:
- 检查
WorkInfo
列表是否不为null并且其中是否包含任何WorkInfo
对象。如果尚未点击Go按钮,则返回。 - 获取列表中的第一个
WorkInfo
;只有一个标记为TAG_OUTPUT
的WorkInfo
,因为我们的工作链是唯一的。 - 使用
workinfo.state.isFinished
检查工作状态是否已完成。 - 如果未完成,请调用
showWorkInProgress()
以隐藏Go按钮并显示Cancel Work按钮和进度条。 - 如果已完成,请调用
showWorkFinished()
以隐藏Cancel Work按钮和进度条,并显示Go按钮。
代码如下:
注意:在收到请求时,导入androidx.lifecycle.Observer
.
BlurActivity.kt
override fun onCreate(saveInstanceState: Bundle?) {
...
// Observe work status, added in onCreate()
viewModel.outputWorkInfos.observe(this, workInfosObverser())
}
// Define the observer function
private fun workInfosObserver(): Observer<List<WorkInfo>> {
return Observer { listOfWorkInfo ->
// Note that these next few lines grab a single WorkInfo if it exists
// This code could be in a Transformation in the ViewModel; they are included here
// so that the entire process of displaying a WorkInfo is in one location.
// If there are no matching work info, do nothing
if (listOfWorkInfo.isNullOrEmpty()) {
return@Observer
}
// We only care about the one output status.
// Every continuation has only one worker tagged TAG_OUTPUT
val workInfo = listOfWorkInfo[0]
if (workInfo.state.isFinished) {
showWorkFinished()
} else {
showWorlInProgress()
}
}
}
8.4、运行您的应用
运行您的应用,它应该编译并运行,且现在可以在工作时显示进度条以取消按钮:
## 九、显示最终输出
每个`WorkInfo`还有一个`getOutputData`方法,该方法可让您获取包含最终保存的图片的输出`Data`对象。在Kotlin中,您可以使用该语言为您生成的变量`outputData`访问此方法。每当有经过模糊处理的图片准备就绪可供显示时,便在屏幕上显示**See File**按钮。
9.1、创建“See File”按钮
activity_blur.xml
布局中有一个隐藏的按钮。它位于BlurActivity
中,名为outputButton
。
在BlurActivity
的onCreate()
中,为该按钮设置点击监听器。此操作应获取URI,然后打开一个activity以查看URI。
BlurActivity.kt
override fun onCreate(savedInstanceState: Bundle?> {
// Setup view output image file button
binding.seeFileButton.setOnClickListener {
viewModel.ourputUri?.let { cuttentUri ->
val actionView = Intent(Intent.ACTION_VIEW, currentUri)
actionView.resolveActivity(packageManager)?.run {
startActivity(actionView)
}
}
}
}
9.2、设置URI并显示按钮
您需要对WorkInfo
观察器应用一些最后的调整,才能达到预期效果:
- 如果
WorkInfo
完成,请使用workInfo.outputData
获取输出数据。 - 然后获取输入URI,请记住,它是使用
Constants.KEY_IMAGE_URI
键存储的。 - 如果URI不为空,则会正确保存;系统会显示
outputButton
并使用该URI对视图模型调用setOutputUri
.
BlurActivity.kt
private fun workInfosObserver(): Observer<List<WorkInfl>> {
return Observer { listOfWorkInfo ->
// Note that these next few lines grab a single WorkInfo if it exists
// This code could be in a Transformation in the ViewModel; they are included here
// so that the entire process of displaying a WorkInfo is in one location
// If there are no matching work info, do nothing
if (listOfWorkInfo.isNullOrEmpty()) {
return@Observer
}
// We only care about the one output status
// Every continuation has only one worker tagged TAG_OUTPUT
val workInfo = listOfWorkInfo[0]
if (workInfo.state.isFinished) {
showWorkFinished()
// Normally this progressing, which is not directly related to drawing views on
// screen would be in the ViewModel. Foe simplicity we are keeping it here.
val outputImageUri = workInfo.outputData.getString(KEY_IMAGE_URI)
// If there is an output file show "See File" button
if (!outputImageUri.isNullOrEmpty()) {
viewModel.setOutputUri(outputImageUri)
binding.seeFileButton.visibility = View.VISIBLE
}
} else {
showWorkInProgress()
}
}
}
9.3、运行您的代码
运行您的代码。您应该会看到新的可点击的See File按钮,该按钮会将您的输出的文件:
十、取消Work
您已添加此取消Work按钮,所以我们要添加一些代码来执行操作。借助WorkManager,您可以使用ID、按标记和唯一链名称取消Work。
在这种情况下,您需要按唯一链名取消工作,因为您想要取消链中的所有工作,而不仅仅是某个特定步骤。
10.1、按名称取消工作
在BlurViewModel
中,添加一个名为cancelWork()
的新方法以取消唯一工作。在函数内,对workManager
调用cancelUniqueWork
,并传入IMAGE_MANIPULATION_WORK_NAME
标记。
BlurViewModel.kt
internal fun cancelWork() {
workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}
10.2、调用取消方法
然后,使用cancelButton
按钮调用cancelWork
:
BlurActivity.kt
// In onCreate()
// Hookup the Cancel button
binding.cancelButton.setOnClickListener { viewModel.cancelWork() }
10.3、运行和取消工作
运行您的应用。它应该可以正常编译。先对图片进行模糊处理,然后点击“取消”按钮。这个链都会被取消!
十一、Work约束
最后,很重要的一点是,WorkManager
支持Constraints
。对于Blur-O-Matic,您将使用设备必须充电的约束条件。也就是说,您的工作请求只会在设备充电的情况下运行。
11、1、创建并添加充电约束条件
如需创建Constraints
对象,请使用Constrainits.Builder
。然后,您可以设置所需的约束条件,并使用方法setRequiresCharging()
将其添加到WorkRequest
:
在收到请求时,导入androidx.work.Constraints
BlurViewModel.kt
// Put this inside the applyBlur() function, above the save work request.
// Create charging constraint
val constraints = Constraints.Builder()
.setRequiresCharging(true)
.build()
// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.setConsteaints(constrains)
.addTag(TAG_OUTPUT)
.build()
continuation = continuation.then(save)
// Actually start the work
continuation.enqueue()
11.2、使用模拟器或设备进行测试
现在您就可以运行Blur-O-Matics了。如果您使用的是一台设备,则可以移除或插入您的设备。在模拟器上,您可以正在“Extended control”(扩展控件)窗口中更改充电状态:
当设备不充电时,应会暂停执行SaveImageToFileWorker
知道您的设备插入充电。