使用WorkManager在后台处理工作 - Kotlin(下)

六、串连各个步骤

现在,您将执行一项工作任务:对图片进行模糊处理。这是非常不错的第一步,但缺少一些核心功能:

  • 此操作不会清理临时文件
  • 实际上它不会将图片保存到永久性文件中
  • 而是始终对图片进行相同程度的模糊处理。

我们将使用WorkManager工作链添加此功能。

WorkManager允许您创建按顺序运行或并行运行的单独WorkRequest。在此步骤中,您将创建一个如下所示的工作链:

WorkRequest表示为方框。

链接的另一个简介功能时,一个WorkRequest的输出会成为链中下一个WorkRequest的输入。在每个WorkRequest之间传递的输入和输出均显示为蓝色文本。

6.1、创建清理和保存工作器

首先,您需要定义所需的所有Worker类。您已经有了用于对图片进行模糊处理的Worker,但还需要用于清理临时文件的Worker以及用于永久保存图片的Worker

请在workers软件包中创建两个扩展Worker的新类。

第一个类的名称为CleanupWorler,第二个类的名称应为SaveImageFileWorker

6.2、扩展工作器

Worker类扩展CleanupWorker类。添加所需的构造函数参数。

arduino 复制代码
class CleanupWorker(ctx: Context, params: WorkerParameters): Worker(ctx, params) {
}

6.3、替换和实现doWork()以用于CleanupWorker

CleanupWorker不需要获取任何输入或传递任何输出。它只是删除临时文件(如果存在)。

CleanupWorker.kt

kotlin 复制代码
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检索resourceUrioutput值。

SaveImageToFileWorker.kt

kotlin 复制代码
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

kotlin 复制代码
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链

您需要修改BlurViewModelapplyBlur方法以执行WorkRequest链,而不是仅执行一个请求。目前,代码如下所示:

BlurViewModel.kt

scss 复制代码
val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
    .setInputData(createInputDataForUri())
    .build()
    
workManager.enqueue(blurRequest)

调用workMnager.beginWith(),而不是调用workManager.enqueue()。此调用会返回WorkContinuation,其定义了WorkRequest链。您可以通过调用then()方法向此工作请求链中添加请求对象。例如,如果您拥有了三个WorkRequest对象,即workAworkBworkC,则可以编写以下代码:

scss 复制代码
val continuation = workManager.beginWith(workA)

continuation.then(workB)
    .then(workC)
    .enqueue()

此代码将生成并运行以下WorkRequest链:

applyBlur中创建一个CleanupWorker WorkRequestBlurImage WorkRequestSaveImageToFile WorkRequest链。将输入传递到BlurImage WorkRequest中。

此操作的代码如下:

BlurViewModel.kt

kotlin 复制代码
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

scss 复制代码
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。选项包括REPLACEKEEPAPPEND

您将使用REPLACE,因为如果用户在当前图片完成之前决定对另一张图片进行模糊处理,我们需要停止当前图片并开始对新图片进行模糊处理。

用于启动唯一工作延续的代码如下:

BlurViewModel.kt

csharp 复制代码
// 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是否为BLOCKEDCANELLEDENQUEUEDFAILEDRUNNINGSUCCEEDED
  • 如果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

ini 复制代码
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
        .addTag(TAG_OUTPUT)
        .build()

8.2、获取WorkInfo

现在您已经标记了工作,可以获取WorkInfo:

  1. BlurViewModel中,声明一个名为outputWorkInfos的新类变量,该变量是LiveData<List<WorkInfo>>
  2. BlurViewModel中添加init块以使用WorkManager.getWorkInfosByTagLiveData获取WorkInfo

您需要的代码如下:

BlurViewModel.kt

swift 复制代码
// 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

现在您已拥有适用于WorkInfoLiveData,可以在BlurActivity中进行观察。在观察器中:

  1. 检查WorkInfo列表是否不为null并且其中是否包含任何WorkInfo对象。如果尚未点击Go按钮,则返回。
  2. 获取列表中的第一个WorkInfo;只有一个标记为TAG_OUTPUTWorkInfo,因为我们的工作链是唯一的。
  3. 使用workinfo.state.isFinished检查工作状态是否已完成。
  4. 如果未完成,请调用showWorkInProgress()以隐藏Go 按钮并显示Cancel Work按钮和进度条。
  5. 如果已完成,请调用showWorkFinished()以隐藏Cancel Work 按钮和进度条,并显示Go按钮。

代码如下: 注意:在收到请求时,导入androidx.lifecycle.Observer.

BlurActivity.kt

kotlin 复制代码
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

BlurActivityonCreate()中,为该按钮设置点击监听器。此操作应获取URI,然后打开一个activity以查看URI。

BlurActivity.kt

kotlin 复制代码
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观察器应用一些最后的调整,才能达到预期效果:

  1. 如果WorkInfo完成,请使用workInfo.outputData获取输出数据。
  2. 然后获取输入URI,请记住,它是使用Constants.KEY_IMAGE_URI键存储的。
  3. 如果URI不为空,则会正确保存;系统会显示outputButton并使用该URI对视图模型调用setOutputUri.

BlurActivity.kt

kotlin 复制代码
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

kotlin 复制代码
internal fun cancelWork() {
    workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}

10.2、调用取消方法

然后,使用cancelButton按钮调用cancelWork:

BlurActivity.kt

scss 复制代码
// 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

scss 复制代码
// 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知道您的设备插入充电。

相关推荐
雨白7 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk7 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING8 小时前
RN容器启动优化实践
android·react native
恋猫de小郭10 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker15 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴15 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭1 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农2 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos