六、串连各个过程

现在,您将履行一项作业任务:对图片进行含糊处理。这是非常不错的第一步,但短少一些中心功用:

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

咱们将运用WorkManager作业链增加此功用。

WorkManager允许您创立按次序运转或并行运转的单独WorkRequest。在此过程中,您将创立一个如下所示的作业链:

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

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。

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

请留意,系统会运用键KEY_IMAGE_URI检索resourceUrioutput值。

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链

您需求修正BlurViewModelapplyBlur办法以履行WorkRequest链,而不是仅履行一个恳求。目前,代码如下所示:

BlurViewModel.kt

val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
    .setInputData(createInputDataForUri())
    .build()
workManager.enqueue(blurRequest)

调用workMnager.beginWith(),而不是调用workManager.enqueue()。此调用会回来WorkContinuation,其界说了WorkRequest链。您能够通过调用then()办法向此作业恳求链中增加恳求目标。例如,假如您拥有了三个WorkRequest目标,即workAworkBworkC,则能够编写以下代码:

val continuation = workManager.beginWith(workA)
continuation.then(workB)
    .then(workC)
    .enqueue()

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

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

applyBlur中创立一个CleanupWorker WorkRequestBlurImage WorkRequestSaveImageToFile 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按钮,并能够在不同作业器运转时看到告诉。您依然能够在设备文件浏览器中检查通过含糊处理的图片,鄙人一步中,您将再增加一个按钮,以便用户能够在设备上检查现已模处理的图片。

鄙人面的屏幕截图中,您会发现告诉消息中显现当时正在运转的作业器。

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

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

### 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。选项包括REPLACEKEEPAPPEND

您将运用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是否为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

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

// 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

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、运转您的运用

运转您的运用,它应该编译并运转,且现在能够在作业时显现进度条以撤销按钮:

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

## 九、显现终究输出 每个`WorkInfo`还有一个`getOutputData`办法,该办法可让您获取包括终究保存的图片的输出`Data`目标。在Kotlin中,您能够运用该言语为您生成的变量`outputData`访问此办法。每当有通过含糊处理的图片准备就绪可供显现时,便在屏幕上显现**See File**按钮。

9.1、创立“See File”按钮

activity_blur.xml布局中有一个躲藏的按钮。它坐落BlurActivity中,名为outputButton

BlurActivityonCreate()中,为该按钮设置点击监听器。此操作应获取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调查器运用一些最终的调整,才干达到预期作用:

  1. 假如WorkInfo完结,请运用workInfo.outputData获取输出数据。
  2. 然后获取输入URI,请记住,它是运用Constants.KEY_IMAGE_URI键存储的。
  3. 假如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按钮,该按钮会将您的输出的文件:

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

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

十、撤销Work

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

您已增加此撤销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、运转和撤销作业

运转您的运用。它应该能够正常编译。先对图片进行含糊处理,然后点击“撤销”按钮。这个链都会被撤销!

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

十一、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”(扩展控件)窗口中更改充电状况:

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

当设备不充电时,应会暂停履行SaveImageToFileWorker知道您的设备刺进充电。

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