10-WorkManager

2023-05-16

WorkManager

文章目录

    • WorkManager
      • 概览
      • 使用入门
        • 将WorkManager添加到项目中
        • 创建后台任务
        • 配置运行任务的方式和时间
        • 将任务提交给系统
        • 后续步骤
      • 方法指南
        • 定义WorkRequest
          • 工作约束
          • 初始延迟
          • 重试和退避政策
          • 定义任务的输入/输出
          • 标记工作
        • 观察工作状态
          • 工作状态
          • 观察工作状态
        • 观察工作器的中间进度
          • 更新进度
          • 观察进度
        • 将工作链接在一起
          • 简介
          • Input Merger
          • 链接和工作状态
        • 取消和停止工作
          • 停止正在运行的工作器
        • 处理重复性工作
        • 处理唯一作业
        • 测试Worker实现
          • 测试ListenableWorker及其子类
          • 测试Workers
        • 使用WorkManager进行集成测试
          • 介绍和设置
          • 概念
          • 结构化测试
            • 基本测试
          • 模拟约束、延迟和周期性工作
            • 测试初始化延迟
            • 测试约束
            • 测试周期性工作
        • 调试WorkManager
      • 高级概念
        • 配置和初始化
          • WorkManager 2.1.0及更高版本
            • 按需初始化
            • 移除默认初始化程序
            • 实现Configration.Provider
          • WorkManager 2.0.1及更早版本
            • 默认初始化
            • 自定义初始化
        • WorkManager中的线程处理
          • 概览
          • 用Worker处理线程
          • 用CoroutineWorker处理线程
          • 用RxWorker处理线程
          • 用ListenableWorker处理线程
        • 支持长时间运行的工作器
          • 创建和管理长时间运行的任务
            • Java
            • Kotlin
      • 从Firebase JobDispatcher迁移
      • 从GCMNetworkManager迁移

概览

使用 WorkManager API 可以轻松地调度即使在应用退出或设备重启时仍应运行的可延迟异步任务。

主要功能

  • 最高向后兼容到 API 14
    • 在运行 API 23 (Android 6.0)及以上级别的设备上使用 JobScheduler
    • 在运行 API 14-22 的设备上结合使用 BroadcastReceiver 和 AlarmManager
  • 添加网络可用性或充电状态等工作约束
  • 调度一次性或周期性异步任务
  • 监控和管理计划任务
  • 将任务链接起来
  • 确保任务执行,即使应用或设备重启也同样执行任务
  • 遵循低电耗模式等省电功能

WorkManager 旨在用于可延迟运行(即不需要立即运行)并且在应用退出或设备重启时必须能够可靠运行的任务。例如:

  • 向后端服务发送日志或分析数据
  • 定期将应用数据与服务器同步

WorkManager 不适用于应用进程结束时能够安全终止的运行中后台工作,也不适用于需要立即执行的任务。请查看后台处理指南,了解哪种解决方案符合您的需求。

使用入门

将WorkManager添加到项目中

    dependencies {
      def work_version = "2.3.1"

        // (Java only)
        implementation "androidx.work:work-runtime:$work_version"

        // Kotlin + coroutines
        implementation "androidx.work:work-runtime-ktx:$work_version"

        // optional - RxJava2 support
        implementation "androidx.work:work-rxjava2:$work_version"

        // optional - GCMNetworkManager support
        implementation "androidx.work:work-gcm:$work_version"

        // optional - Test helpers
        androidTestImplementation "androidx.work:work-testing:$work_version"
      }

创建后台任务

任务是使用 Worker 类定义的。doWork() 方法在 WorkManager 提供的后台线程上同步运行。

要创建后台任务,请扩展 Worker 类并替换 doWork() 方法。

例如,要创建上传图像的 Worker,您可以执行以下操作:

    class UploadWorker(appContext: Context, workerParams: WorkerParameters)
        : Worker(appContext, workerParams) {

        override fun doWork(): Result {
            // Do the work here--in this case, upload the images.

            uploadImages()

            // Indicate whether the task finished successfully with the Result
            return Result.success()
        }
    }    

doWork() 返回的 Result 会通知 WorkManager 任务是否:

  • 已成功完成:Result.success()

  • 已失败:Result.failure()

  • 需要稍后重试:Result.retry()

配置运行任务的方式和时间

Worker 定义工作单元,WorkRequest 则定义工作的运行方式和时间。

任务可以是一次性的,也可以是周期性的。

  • 一次性 WorkRequest,请使用 OneTimeWorkRequest;

  • 周期性工作,请使用 PeriodicWorkRequest

在本例中,为 UploadWorker 构建 WorkRequest 最简单的示例为:

    val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>().build()

WorkRequest 中还可以包含其他信息,例如任务在运行时应遵循的约束、工作输入、延迟,以及重试工作的退避时间政策。

将任务提交给系统

定义 WorkRequest 之后,您现在可以通过 WorkManager 使用 enqueue() 方法来调度它。

    WorkManager.getInstance(myContext).enqueue(uploadWorkRequest)

后续步骤

  • 查看 WorkManager 在简单的图像处理应用中的运行情况
  • 使用 Kotlin、Java 在 codelab 中进行实操练习
  • 了解如何添加工作约束。

方法指南

定义WorkRequest

工作约束

您可以向工作添加 Constraints,以指明工作何时可以运行。

例如,您可以指定工作应仅在设备空闲且接通电源时运行。

下面的代码展示了如何将这些约束添加到 OneTimeWorkRequest。有关所支持约束的完整列表,请参阅 Constraints.Builder 参考文档。

    // Create a Constraints object that defines when the task should run
    val constraints = Constraints.Builder()
            .setRequiresDeviceIdle(true)
            .setRequiresCharging(true)
            .build()

    // ...then create a OneTimeWorkRequest that uses those constraints
    val compressionWork = OneTimeWorkRequestBuilder<CompressWorker>().setConstraints(constraints).build()

如果指定了多个约束,您的任务将仅在满足所有约束时才会运行。

如果在任务运行期间某个约束不再得到满足,则 WorkManager 将停止工作器。当约束继续得到满足时,系统将重新尝试执行该任务。

初始延迟

如果您的工作没有约束,或者工作加入队列时所有约束均已得到满足,则系统可能会选择立即运行任务。如果您不希望任务立即运行,则可以将工作指定为在经过最短的初始延迟后启动。

下面的示例展示了如何将任务设置为在加入队列后至少经过 10 分钟再运行。

    val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
            .setInitialDelay(10, TimeUnit.MINUTES)
            .build()

注意:执行工作器的确切时间还取决于 WorkRequest 中使用的约束和系统优化。WorkManager 经过设计,能够在满足这些约束的情况下提供可能的最佳行为。

重试和退避政策

如果您需要让 WorkManager 重新尝试执行您的任务,可以从工作器返回 Result.retry()

然后,系统会根据默认的退避延迟时间和政策重新调度您的工作。退避延迟时间指定重试工作前的最短等待时间。退避政策定义了在后续重试的尝试过程中,退避延迟时间随时间以怎样的方式增长;默认情况下按 EXPONENTIAL 延长。

以下是自定义退避延迟时间和政策的示例。

		val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
            .setBackoffCriteria(
                    BackoffPolicy.LINEAR,
                    OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
                    TimeUnit.MILLISECONDS)
            .build()

Criteria 标准, 准则, 规范

定义任务的输入/输出

任务可能需要数据以输入参数的形式传入,或者将数据返回为结果。例如,某个任务负责处理图像上传,它要求以要上传的图像的 URI 为输入,并且可能要求用已上传图像的网址作为输出。

输入和输出值以键值对的形式存储在 Data 对象中。下面的代码展示了如何在 WorkRequest 中设置输入数据。

    // workDataOf (part of KTX) converts a list of pairs to a [Data] object.
    val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)

    val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
            .setInputData(imageData)
            .build()

Worker 类可通过调用 Worker.getInputData() 访问输入参数。

类似地,Data 类可用于输出返回值。要返回 Data 对象,请将它包含到 ResultResult.success()Result.failure() 中,如下所示。

    class UploadWorker(appContext: Context, workerParams: WorkerParameters)
        : Worker(appContext, workerParams) {

        override fun doWork(): Result {

                // Get the input
                val imageUriInput = getInputData().getString(Constants.KEY_IMAGE_URI)
                // TODO: validate inputs.
                // Do the work
                val response = uploadFile(imageUriInput)

                // Create the output of the work
                val outputData = workDataOf(Constants.KEY_IMAGE_URL to response.imageUrl)

                // Return the output
                return Result.success(outputData)

        }
    }

注意:按照设计,Data 对象应该很小,值可以是字符串、基元类型或数组变体。如果需要将更多数据传入和传出工作器,应该将数据放在其他位置,例如 Room 数据库。Data 对象的大小上限为 10KB。

标记工作

可以通过为任意 WorkRequest 对象分配标记字符串,按逻辑对任务进行分组。这样就可以对使用特定标记的所有任务执行操作。

例如,WorkManager.cancelAllWorkByTag(String) 会取消使用特定标记的所有任务,而 WorkManager.getWorkInfosByTagLiveData(String) 会返回 LiveData 和具有该标记的所有任务的状态列表。

以下代码展示了如何使用 WorkRequest.Builder.addTag(String) 向任务添加“cleanup”标记:

    val cacheCleanupTask =
            OneTimeWorkRequestBuilder<CacheCleanupWorker>()
        .setConstraints(constraints)
        .addTag("cleanup")
        .build()

观察工作状态

工作状态

在工作的整个生命周期内,它会经历多个不同的 State

  • 如果有尚未完成的前提性工作,则工作处于 BLOCKED State
  • 如果工作能够在满足 Constraints 和时机条件后立即运行,则被视为处于 ENQUEUED 状态。
  • 当工作器在活跃地执行时,其处于 RUNNING State
  • 如果工作器返回 Result.success(),则被视为处于 SUCCEEDED 状态。这是一种终止 State;只有 OneTimeWorkRequest 可以进入这种 State
  • 相反,如果工作器返回 Result.failure(),则被视为处于 FAILED 状态。这也是一个终止 State;只有 OneTimeWorkRequest 可以进入这种 State。所有依赖工作也会被标记为 FAILED,并且不会运行。
  • 当您明确取消尚未终止的 WorkRequest 时,它会进入 CANCELLED State。所有依赖工作也会被标记为 CANCELLED,并且不会运行。
观察工作状态

将工作加入队列后,您可以通过 WorkManager 检查其状态。相关信息在 WorkInfo 对象中提供,包括工作的 id、标签、当前 State 和任何输出数据。

您通过以下三种方式之一来获取 WorkInfo

  • 对于特定的 WorkRequest,您可以利用 WorkManager.getWorkInfoById(UUID)WorkManager.getWorkInfoByIdLiveData(UUID) 来通过 WorkRequest id 检索其 WorkInfo
  • 对于指定的标记,您可以利用 WorkManager.getWorkInfosByTag(String)WorkManager.getWorkInfosByTagLiveData(String) 检索所有匹配的 WorkRequestWorkInfo 对象。
  • 对于唯一工作名称,您可以利用 WorkManager.getWorkInfosForUniqueWork(String)WorkManager.getWorkInfosForUniqueWorkLiveData(String) 检索所有匹配的 WorkRequestWorkInfo 对象。

利用每个方法的 LiveData 变量,您可以通过注册监听器来观察 WorkInfo 的变化。例如,如果您想要在某项工作成功完成后向用户显示消息,您可以进行如下设置:

    WorkManager.getInstance(myContext).getWorkInfoByIdLiveData(uploadWorkRequest.id)
            .observe(lifecycleOwner, Observer { workInfo ->
                if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
                    displayMessage("Work finished!")
                }
            })

观察工作器的中间进度

WorkManager 2.3.0-alpha01 为设置和观察工作器的中间进度添加了一流支持。如果应用在前台运行时,工作器保持运行状态,也可以使用返回 WorkInfoLiveData 的 API 向用户显示此信息。

ListenableWorker 现在支持 setProgressAsync() API,此类 API 可以保留中间进度。借助这些 API,开发者能够设置可通过界面观察到的中间进度。进度由 Data 类型表示,这是一个可序列化的属性容器(类似于 inputoutput,并且受到相同的限制)。

只有在 ListenableWorker 运行时才能观察到和更新进度信息。如果尝试在 ListenableWorker 完成执行后在其中设置进度,则将会被忽略。您还可以使用 getWorkInfoBy…()getWorkInfoBy…LiveData() 方法来观察进度信息。这两个方法会返回 WorkInfo 的实例,后者有一个返回 Data 的新 getProgress() 方法。

更新进度

对于使用 ListenableWorkerWorker 的 Java 开发者,setProgressAsync() API 会返回 ListenableFuture<Void>;更新进度是异步过程,因为更新过程包括将进度信息存储在数据库中。在 Kotlin 中,您可以使用 CoroutineWorker 对象的 setProgress() 扩展函数来更新进度信息。

此示例展示了一个简单的 ProgressWorker。该 Worker 启动时将进度设置为 0,完成时将进度值更新为 100。

    import android.content.Context
    import androidx.work.CoroutineWorker
    import androidx.work.Data
    import androidx.work.WorkerParameters
    import kotlinx.coroutines.delay

    class ProgressWorker(context: Context, parameters: WorkerParameters) :
        CoroutineWorker(context, parameters) {

        companion object {
            const val Progress = "Progress"
            private const val delayDuration = 1L
        }

        override suspend fun doWork(): Result {
            val firstUpdate = workDataOf(Progress to 0)
            val lastUpdate = workDataOf(Progress to 100)
            setProgress(firstUpdate)
            delay(delayDuration)
            setProgress(lastUpdate)
            return Result.success()
        }
    }
观察进度

观察进度信息也很简单。您可以使用 getWorkInfoBy…()getWorkInfoBy…LiveData() 方法,并引用 WorkInfo

以下是使用 getWorkInfoByIdLiveData API 的示例。

    WorkManager.getInstance(applicationContext)
        // requestId is the WorkRequest id
        .getWorkInfoByIdLiveData(requestId)
        .observe(observer, Observer { workInfo: WorkInfo? ->
                if (workInfo != null) {
                    val progress = workInfo.progress
                    val value = progress.getInt(Progress, 0)
                    // Do something with progress information
                }
        })

将工作链接在一起

简介

使用 WorkManager 创建工作链并为其排队。工作链用于指定多个关联任务并定义这些任务的运行顺序。当您需要以特定的顺序运行多个任务时,这尤其有用。

要创建工作链,您可以使用 WorkManager.beginWith(OneTimeWorkRequest)WorkManager.beginWith(List),这会返回 WorkContinuation 实例。

然后,可以通过 WorkContinuation 使用 WorkContinuation.then(OneTimeWorkRequest)WorkContinuation.then(List) 来添加从属 OneTimeWorkRequest

每次调用 WorkContinuation.then(...) 都会返回一个新的 WorkContinuation 实例。如果添加了 OneTimeWorkRequestList,这些请求可能会并行运行。

最后,您可以使用 WorkContinuation.enqueue() 方法为 WorkContinuation 链排队。

让我们看一个示例:某个应用对 3 个不同的图像执行图像滤镜(可能会并行执行),然后将这些图像压缩在一起,再上传它们。

    WorkManager.getInstance(myContext)
        // Candidates to run in parallel
        .beginWith(listOf(filter1, filter2, filter3))
        // Dependent work (only runs after all previous work in chain)
        .then(compress)
        .then(upload)
        // Don't forget to enqueue()
        .enqueue()
Input Merger

在使用 OneTimeWorkRequest 链时,父级 OneTimeWorkRequest 的输出将作为输入传递给子级。因此在上面的示例中,filter1filter2filter3 的输出将作为输入传递给 compress 请求。

为了管理来自多个父级 OneTimeWorkRequest 的输入,WorkManager 使用 InputMerger

WorkManager 提供两种不同类型的 InputMerger

  • OverwritingInputMerger 会尝试将所有输入中的所有键添加到输出中。如果发生冲突,它会覆盖先前设置的键。
  • ArrayCreatingInputMerger 会尝试合并输入,并在必要时创建数组。

对于上面的示例,假设我们要保留所有图像滤镜的输出,则应使用 ArrayCreatingInputMerger

    val compress: OneTimeWorkRequest = OneTimeWorkRequestBuilder<CompressWorker>()
        .setInputMerger(ArrayCreatingInputMerger::class)
        .setConstraints(constraints)
        .build()
链接和工作状态

创建 OneTimeWorkRequest 链时,需要注意以下几点:

  • 从属 OneTimeWorkRequest 仅在其所有父级 OneTimeWorkRequest 都成功完成(即返回 Result.success())时才会被解除阻塞(变为 ENQUEUED 状态)。
  • 如果有任何父级 OneTimeWorkRequest 失败(返回 Result.failure()),则所有从属 OneTimeWorkRequest 也会被标记为 FAILED
  • 如果有任何父级 OneTimeWorkRequest 被取消,则所有从属 OneTimeWorkRequest 也会被标记为 CANCELLED

取消和停止工作

如果您不再需要运行先前加入队列的作业,则可以申请取消。最简单的方法是使用其 id 并调用 WorkManager.cancelWorkById(UUID) 来取消单个 WorkRequest:

    WorkManager.cancelWorkById(workRequest.id)

在后台,WorkManager 会检查工作的 State。如果工作已经完成,则不会发生任何变化。否则,其状态将更改为 CANCELLED,之后就不会运行这个工作。任何依赖于这项工作的 WorkRequests 的状态也将变为 CANCELLED

此外,如果工作当前的状态为 RUNNING,则工作器也会收到对 ListenableWorker.onStopped() 的调用。替换此方法以处理任何可能的清理操作。我们会在下文详细讨论相关内容。

您也可以使用 WorkManager.cancelAllWorkByTag(String) 按标记取消 WorkRequest。请注意,此方法会取消所有具有此标记的工作。此外,您还可以使用 WorkManager.cancelUniqueWork(String) 取消具有唯一名称的所有工作。

停止正在运行的工作器

WorkManager 停止正在运行的工作器可能有几种不同的原因:

  • 您明确要求取消它(例如,通过调用 WorkManager.cancelWorkById(UUID) 取消)。
  • 如果是唯一工作,使用 ExistingWorkPolicy REPLACE 明确地将新的 WorkRequest 加入队列。旧的 WorkRequest 会立即被视为已终止。
  • 您的工作约束已不再得到满足。
  • 系统出于某种原因指示您的应用停止工作。如果超过 10 分钟的执行期限,可能会发生这种情况。系统将工作安排在稍后重试。

在这些情况下,您的员工会收到对 ListenableWorker.onStopped() 的调用。如果操作系统决定关闭您的应用,您应执行清理工作并以协作方式完成工作器。例如,您应该在此时或者尽早关闭数据库和文件的打开句柄。此外,如果您想要确认系统是否已经停止您的应用,都可以调用 ListenableWorker.isStopped()。即使您通过在调用 onStopped() 后返回 Result 来指示工作已完成,WorkManager 都会忽略该 Result,因为工作器已经被视为停止。

处理重复性工作

应用场景:

应用有时可能需要定期运行某些任务。例如,您可能要定期备份数据、下载应用中的新鲜内容,或者上传日志到服务器。将 PeriodicWorkRequest 用于这种需要定期执行的任务。

PeriodicWorkRequest 无法链接。如果您的任务需要链接任务,请考虑 OneTimeWorkRequest

您可以按照以下方式创建 PeriodicWorkRequest:

    val constraints = Constraints.Builder()
            .setRequiresCharging(true)
            .build()

    val saveRequest =
    PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
        .setConstraints(constraints)
        .build()

    WorkManager.getInstance(myContext)
        .enqueue(saveRequest)

示例中展示了一个重复间隔为一小时的定期工作请求。

重复间隔定义为重复之间的最短时间。工作器的确切执行时间取决于您在工作请求中使用的约束,也取决于系统进行的优化。

在示例中,PeriodicWorkRequest 还要求设备接通电源。在这种情况下,即使过了定义的一小时重复间隔,PeriodicWorkRequest 也将在设备接通电源时运行。

注意:可以定义的最短重复间隔是 15 分钟(与 JobScheduler API 相同)。

处理唯一作业

唯一工作是一个概念性非常强的术语,可确保一次只有一个具有特定名称的工作链。与 id 不同的是,唯一名称是人类可读的,由开发者指定,而不是由 WorkManager 自动生成。与标记不同,唯一名称仅与“一个”工作链关联。

您可以通过调用 [WorkManager.enqueueUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)](https://developer.android.com/reference/androidx/work/WorkManager#enqueueUniqueWork(java.lang.String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest)) 或 [WorkManager.enqueueUniquePeriodicWork(String, ExistingPeriodicWorkPolicy, PeriodicWorkRequest)](https://developer.android.com/reference/androidx/work/WorkManager#enqueueUniquePeriodicWork(java.lang.String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest)) 创建唯一工作序列。第一个参数是唯一名称 - 这是我们用来标识 WorkRequest 的键。第二个参数是冲突解决策略,它指定了如果已经存在一个具有该唯一名称的未完成工作链,WorkManager 应该如何处理:

  • 取消现有工作链,并将其 REPLACE 为新工作链。
  • KEEP 现有序列并忽略您的新请求。
  • 将新序列 APPEND 到现有序列,在现有序列的最后一个任务完成后运行新序列的第一个任务。您不能将 APPENDPeriodicWorkRequest 一起使用。

当您有不能够多次排队的任务时,唯一工作将非常有用。例如,如果您的应用需要将其数据同步到网络,您可能需要对一个名为“sync”的序列进行排队,并指定当已经存在具有该名称的序列时,应该忽略新的任务。

当您需要逐步构建一个长任务链时,也可以利用唯一工作序列。例如,照片编辑应用可能允许用户撤消一长串操作。其中的每一项撤消操作可能都需要一些时间来完成,但必须按正确的顺序执行。在这种情况下,应用可以创建一个“撤消”链,并根据需要将每个撤消操作附加到该链上。

最后,如果您需要创建一个唯一工作链,可以使用 [WorkManager.beginUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)](https://developer.android.com/reference/androidx/work/WorkManager?hl=en#beginUniqueWork(java.lang.String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest)) 代替 beginWith()

测试Worker实现

从2.1.0版本开始,WorkerManager提供了APIs,用来测试Woker,ListenableWorker,以及ListenableWorker的子类(CoroutineWorker / RxWorker / Worker)

在V2.1.0之前,如果要测试Workers,需要使用WorkManagerTestInitHelper去初始化WorkerManager。通过V2.1.0,不需要WorkerManagerTestInitHelper,就可以测试Woker的实现。

测试ListenableWorker及其子类

测试ListenableWorker或者它的子类(CoroutineWorker / RxWorker),使用TestListenableWorkerBuilder。这个构建器可以帮助构建ListenableWorker的实例,用于测试Worker的业务逻辑。

比如,假设我们需要测试一个如下的CoroutineWorker:

class SleepWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {
    override suspend fun doWork(): Result {
        delay(1000) // milliseconds
        return Result.success()
    }
}

为了测试SleepWorker,我们首先通过TestListenableWorkerBuilder创建Worker的实例。这个构建器也可以用来设置标签、inputData、runAttemptCount等等。具体请参考 TestListenableWorker

@RunWith(AndroidJUnit4::class)
class SleepWorkerTest {
    private lateinit var context: Context

    @Before
    fun setUp() {
        context = ApplicationProvider.getApplicationContext()
    }

    @Test
    fun testSleepWorker() {
        // Kotlin code can use the TestListenableWorkerBuilder extension to
        // build the ListenableWorker
        val worker = TestListenableWorkerBuilder<SleepWorker>(context).build()
        runBlocking {
            val result = worker.doWork()
            assertThat(result, `is`(Result.success()))
        }
    }
}
测试Workers

假设我们有一个Worker,如下所示

class SleepWorker(context: Context, parameters: WorkerParameters) :
    Worker(context, parameters) {

    companion object {
        const val SLEEP_DURATION = "SLEEP_DURATION"
    }

    override fun doWork(): Result {
        // Sleep on a background thread.
        val sleepDuration = inputData.getLong(SLEEP_DURATION, 1000)
        Thread.sleep(sleepDuration)
        return Result.success()
    }
}

为了测试这个Worker,现在我们可以使用TestWorkerBuilder。TestWorkerBuilder和TestListenableWorkerBuilder的主要区别在于,TestWorkerBuilder允许你指定Executor来运行Worker

// Kotlin code can use the TestWorkerBuilder extension to
// build the Worker
@RunWith(AndroidJUnit4::class)
class SleepWorkerTest {
    private lateinit var context: Context
    private lateinit var executor: Executor

    @Before
    fun setUp() {
        context = ApplicationProvider.getApplicationContext()
        executor = Executors.newSingleThreadExecutor()
    }

    @Test
    fun testSleepWorker() {
        val worker = TestWorkerBuilder<SleepWorker>(
            context = context,
            executor = executor,
            inputData = workDataOf("SLEEP_DURATION" to 10000L)
        ).build()

        val result = worker.doWork()
        assertThat(result, `is`(Result.success()))
    }
}

使用WorkManager进行集成测试

介绍和设置

WorkManager提供了一个work-testing神器,可以帮助Wokers进行Android Instrumentation测试的单元测试。

为了使用work-testing神器,需要在build.gradle添加依赖

 // optional - Test helpers
        androidTestImplementation "androidx.work:work-testing:$work_version"     

注意:从2.1.0开始,WorkManager提供了新的TestWorkerBuilder和TestListenableWorkerBuilder类,这两个类可以让你在不需要用WorkManagerTestInitHelper初始化WorkManager的情况下,就可以测试Worker中的业务逻辑。本页中的材料对于你需要在Worker实现之外进行Worker测试时仍然很有用。

概念

测试模式下,work-testing提供了WorkerManager的特殊实现,通过WorkerManagerTestInitHelper初始化。

work-testing还提供了一个SynchronousExecutor,它让我们可以更容易地以同步的方式编写测试,而不需要处理多个线程、锁或锁的问题。

通过下面的例子,来展示一起使用这些类:

@RunWith(AndroidJUnit4::class)
class BasicInstrumentationTest {
    @Before
    fun setup() {
        val context = InstrumentationRegistry.getTargetContext()
        val config = 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(context, config)
    }
}
结构化测试

假设我们有一个EchoWorker,期望有一些输入数据,并简单地将其复制(回传)到其输出数据

class EchoWorker(context: Context, parameters: WorkerParameters)
   : Worker(context, parameters) {
   override fun doWork(): Result {
       return when(inputData.size()) {
           0 -> Result.failure()
           else -> Result.success(inputData)
       }
   }
}
基本测试

​ 下面是一个测试EchoWorker的Android Instrumentation测试。这里的要点是,在测试模式下测试EchoWorker与实际应用中使用EchoWorker的方式非常相似。

@Test
@Throws(Exception::class)
fun testSimpleEchoWorker() {
    // Define input data
    val input = workDataOf(KEY_1 to 1, KEY_2 to 2)

    // Create request
    val request = OneTimeWorkRequestBuilder<EchoWorker>()
        .setInputData(input)
        .build()

    val workManager = WorkManager.getInstance(applicationContext)
    // Enqueue and wait for result. This also runs the Worker synchronously
    // because we are using a SynchronousExecutor.
    workManager.enqueue(request).result.get()
    // Get WorkInfo and outputData
    val workInfo = workManager.getWorkInfoById(request.id).get()
    val outputData = workInfo.outputData
    // Assert
    assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    assertThat(outputData, `is`(input))
}

当EchoWorker没有获取到任何输入数据的时候,我们希望返回的Result是Result.failure()

@Test
@Throws(Exception::class)
fun testEchoWorkerNoInput() {
   // Create request
   val request = OneTimeWorkRequestBuilder<EchoWorker>()
       .build()

   val workManager = WorkManager.getInstance(applicationContext)
   // Enqueue and wait for result. This also runs the Worker synchronously
   // because we are using a SynchronousExecutor.
   workManager.enqueue(request).result.get()
   // Get WorkInfo
   val workInfo = workManager.getWorkInfoById(request.id).get()
   // Assert
   assertThat(workInfo.state, `is`(WorkInfo.State.FAILED))
}
模拟约束、延迟和周期性工作

WorkManagerTestInitHelper为你提供了一个TestDriver的实例,它可以用来模拟初始化延迟、ListenableWorkers满足约束的条件,以及PeriodicWorkRequests的时间间隔。

测试初始化延迟

Worker可以有初始延迟。要测试EchoWorker的初始延迟,相比于在测试中等待初始延迟,我们更倾向于使用TestDriver将WorkRequests的初始延迟标记为满足。

@Test
@Throws(Exception::class)
fun testWithInitialDelay() {
    // Define input data
    val input = workDataOf(KEY_1 to 1, KEY_2 to 2)

    // Create request
    val request = OneTimeWorkRequestBuilder<EchoWorker>()
        .setInputData(input)
        .setInitialDelay(10, TimeUnit.SECONDS)
        .build()

    val workManager = WorkManager.getInstance(getApplicationContext())
    val testDriver = WorkManagerTestInitHelper.getTestDriver()
    // Enqueue and wait for result.
    workManager.enqueue(request).result.get()
    testDriver.setInitialDelayMet(request.id)
    // Get WorkInfo and outputData
    val workInfo = workManager.getWorkInfoById(request.id).get()
    val outputData = workInfo.outputData
    // Assert
    assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    assertThat(outputData, `is`(input))
}
测试约束

TestDriver通过setAllConstraintsMet方法,可以标记约束条件被满足。

下面是一个关于如何用约束来测试Worker的例子。

@Test
@Throws(Exception::class)
fun testWithConstraints() {
    // Define input data
    val input = workDataOf(KEY_1 to 1, KEY_2 to 2)

    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build()

    // Create request
    val request = OneTimeWorkRequestBuilder<EchoWorker>()
        .setInputData(input)
        .setConstraints(constraints)
        .build()

    val workManager = WorkManager.getInstance(myContext)
    val testDriver = WorkManagerTestInitHelper.getTestDriver()
    // Enqueue and wait for result.
    workManager.enqueue(request).result.get()
    testDriver.setAllConstraintsMet(request.id)
    // Get WorkInfo and outputData
    val workInfo = workManager.getWorkInfoById(request.id).get()
    val outputData = workInfo.outputData
    // Assert
    assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    assertThat(outputData, `is`(input))
}
测试周期性工作

TestDriver还公开了一个setPeriodDelayMet,它可以用来表示一个间隔已经完成。

下面是一个使用setPeriodDelayMet的例子。

@Test
@Throws(Exception::class)
fun testPeriodicWork() {
    // Define input data
    val input = workDataOf(KEY_1 to 1, KEY_2 to 2)

    // Create request
    val request = PeriodicWorkRequestBuilder<EchoWorker>(15, MINUTES)
        .setInputData(input)
        .build()

    val workManager = WorkManager.getInstance(myContext)
    val testDriver = WorkManagerTestInitHelper.getTestDriver()
    // Enqueue and wait for result.
    workManager.enqueue(request).result.get()
    // Tells the testing framework the period delay is met
    testDriver.setPeriodDelayMet(request.id)
    // Get WorkInfo and outputData
    val workInfo = workManager.getWorkInfoById(request.id).get()
    // Assert
    assertThat(workInfo.state, `is`(WorkInfo.State.ENQUEUED))
}

调试WorkManager

高级概念

配置和初始化

默认情况下,当您的应用启动时,WorkManager 使用适合大多数应用的合理选项自动进行配置。如果您需要进一步控制 WorkManager 管理和调度工作的方式,可以通过自己初始化 WorkManager 自定义 WorkManager 配置。

WorkManager 2.1.0及更高版本

WorkManager 2.1.0 有多种配置 WorkManager 的方式。为 WorkManager 提供自定义初始化的最灵活方式是使用 WorkManager 2.1.0 及更高版本中提供的按需初始化

按需初始化

通过按需初始化,您可以仅在需要 WorkManager 时创建该组件,而不必每次应用启动时都创建。这样做可将 WorkManager 从关键启动路径中移出,从而提高应用启动性能。

移除默认初始化程序

要提供自己的配置,必须先移除默认初始化程序。为此,请使用合并规则 tools:node="remove" 更新 AndroidManifest.xml

<provider
        android:name="androidx.work.impl.WorkManagerInitializer"
        android:authorities="${applicationId}.workmanager-init"
        tools:node="remove" />
    

要详细了解如何在清单中使用合并规则,请参阅有关合并多个清单文件的文档。

实现Configration.Provider

让您的 Application 类实现 Configuration.Provider 接口,并提供您自己的 Configuration.Provider.getWorkManagerConfiguration() 实现。

当您需要使用 WorkManager 时,请确保调用方法 WorkManager.getInstance(Context)。WorkManager 调用应用的自定义 getWorkManagerConfiguration() 方法来发现其 Configuration。(您无需自己调用 WorkManager.initialize()。)

注意:如果您在 WorkManager 初始化之前调用已弃用的无参数 WorkManager.getInstance() 方法,该方法将抛出异常。您应始终使用 WorkManager.getInstance(Context) 方法,即使您不自定义 WorkManager。

以下示例展示了自定义 getWorkManagerConfiguration() 实现:

    class myApplication() : Application(), Configuration.Provider {
         override getWorkManagerConfiguration() =
               Configuration.Builder()
                    .setMinimumLoggingLevel(android.util.Log.INFO)
                    .build()
    }
WorkManager 2.0.1及更早版本
默认初始化

当您的应用启动时,WorkManager 使用自定义 ContentProvider 进行初始化。此代码位于内部类 androidx.work.impl.WorkManagerInitializer 中,并使用默认 Configuration。自动使用默认初始化程序(除非明确停用它)。默认初始化程序适合大多数应用。

自定义初始化

如果您想控制初始化进程,则必须停用默认初始化程序,然后定义您自己的自定义配置。

移除默认初始化程序后,可以手动初始化 WorkManager:

    // provide custom configuration
    val myConfig = Configuration.Builder()
        .setMinimumLoggingLevel(android.util.Log.INFO)
        .build()

    // initialize WorkManager
    WorkManager.initialize(this, myConfig)
    

WorkManager 单例。确保初始化在 Application.onCreate()ContentProvider.onCreate() 中运行。

有关可用自定义的完整列表,请参阅 Configuration.Builder() 参考文档。

WorkManager中的线程处理

概览

WorkManager 提供了四种不同类型的工作基元:

  • Worker 是最简单的实现,前面几节已经有所介绍。WorkManager 会在后台线程上自动运行它(您可以将它替换掉)。请参阅工作器中的线程处理,详细了解 Worker 中的线程处理。
  • 建议 Kotlin 用户实现 CoroutineWorkerCoroutineWorker 针对后台工作公开挂起函数。默认情况下,它们运行默认的 Dispatcher,您可以对其进行自定义。请参阅 CorventineWorker 中的线程处理,详细了解 CoroutineWorker 中的线程处理。
  • 建议 RxJava2 用户实现 RxWorker。如果您有很多现有异步代码是用 RxJava 建模的,则应使用 RxWirkers。与所有 RxJava2 概念一样,您可以自由选择所需的线程处理策略。请参阅 RxWorker 中的线程处理,详细了解 RxWorker 中的线程处理。
  • ListenableWorkerWorkerCoroutineWorkerRxWorker 的基类。该类专为需要与基于回调的异步 API(例如 FusedLocationProviderClient)进行交互并且不使用 RxJava2 的 Java 开发者而设计。请参阅 ListenableWorker 中的线程处理,详细了解 ListenableWorker 中的线程处理。
用Worker处理线程

​ 当您使用 Worker 时,WorkManager 会自动在后台线程中调用 Worker.doWork()。该后台线程来自于 WorkManager 的 Configuration 中指定的 Executor

​ 默认情况下,WorkManager 会为您设置 Executor,但您也可以自己进行自定义。

​ 例如,您可以在应用中共享现有的后台 Executor,也可以创建单线程 Executor 以确保所有后台工作都按顺序执行,甚至可以指定一个具有不同线程数的 ThreadPool

​ 要自定义 Executor,请确保您已启用 WorkManager 的手动初始化。在配置 WorkManager 时,您可以按以下方式指定 Executor

    WorkManager.initialize(
        context,
        Configuration.Builder()
            .setExecutor(Executors.newFixedThreadPool(8))
            .build())

下面是一个简单的工作器示例,它按顺序下载某些网站的内容:

    class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

        override fun doWork(): ListenableWorker.Result {
            for (i in 0..99) {
                try {
                    downloadSynchronously("https://www.google.com")
                } catch (e: IOException) {
                    return ListenableWorker.Result.failure()
                }
            }

            return ListenableWorker.Result.success()
        }
    }

请注意,Worker.doWork() 是同步调用 - 您将会以阻塞方式完成整个后台工作,并在方法退出时完成工作。如果您在 doWork() 中调用异步 API 并返回 Result,则回调可能无法正常运行。如果您遇到这种情况,请考虑使用 ListenableWorker(请参阅在 ListenableWorker 中进行线程处理)。

当前正在运行的 Worker 因为任何原因而停止时,它会收到对 Worker.onStopped() 的调用。替换此方法或在代码的检查点处调用 Worker.isStopped(),并在必要时释放资源。当上述示例中的 Worker 被停止时,下载项目可能才下载了一半,并且会继续下载,即使已经被停止也不受影响。要优化此行为,您可以执行以下操作:

    class DownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

        override fun doWork(): ListenableWorker.Result {
            for (i in 0..99) {
                if (isStopped) {
                    break
                }

                try {
                    downloadSynchronously("https://www.google.com")
                } catch (e: IOException) {
                    return ListenableWorker.Result.failure()
                }

            }

            return ListenableWorker.Result.success()
        }
    }

Worker 停止后,从 Worker.doWork() 返回什么已不重要;Result 将被忽略。

用CoroutineWorker处理线程

对于 Kotlin 用户,WorkManager 为协程提供了一流的支持。要开始使用,请将 work-runtime-ktx 包含到您的 gradle 文件中。不要扩展 Worker,而应扩展 CoroutineWorker,后者使用的 API 略有不同。例如,如果要构建简单的 CoroutineWorker 来执行某些网络操作,则需要执行以下操作:

    class CoroutineDownloadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

        override suspend fun doWork(): Result = coroutineScope {
            val jobs = (0 until 100).map {
                async {
                    downloadSynchronously("https://www.google.com")
                }
            }

            // awaitAll will throw an exception if a download fails, which CoroutineWorker will treat as a failure
            jobs.awaitAll()
            Result.success()
        }
    }

请注意,CoroutineWorker.doWork() 是一个“挂起”函数。不同于 Worker,此代码不会在 Configuration 中指定的 Executor 上运行,而是默认为 Dispatchers.Default。您可以提供自己的 CoroutineContext 来自定义这个行为。在上面的示例中,您可能希望在 Dispatchers.IO 上完成此操作,如下所示:

    class CoroutineDownloadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

        override val coroutineContext = Dispatchers.IO

        override suspend fun doWork(): Result = coroutineScope {
            val jobs = (0 until 100).map {
                async {
                    downloadSynchronously("https://www.google.com")
                }
            }

            // awaitAll will throw an exception if a download fails, which CoroutineWorker will treat as a failure
            jobs.awaitAll()
            Result.success()
        }
    }

CoroutineWorker 通过取消协程并传播取消信号来自动处理停工情况。您无需执行任何特殊操作来处理停工情况。

用RxWorker处理线程

我们提供了 WorkManager 与 RxJava2 之间的互操作性。要开始使用这种互操作性,除了 work-runtime,还应将 work-rxjava2 包含到 gradle 文件中。然后,您应该扩展 RxWorker,而不是扩展 Worker。最后替换 RxWorker.createWork() 方法以返回 Single<Result>,用于表示您执行的 Result,如下所示:

    public class RxDownloadWorker extends RxWorker {

        public RxDownloadWorker(Context context, WorkerParameters params) {
            super(context, params);
        }

        @Override
        public Single<Result> createWork() {
            return Observable.range(0, 100)
                .flatMap { download("https://www.google.com") }
                .toList()
                .map { Result.success() };
        }
    }
    

请注意,RxWorker.createWork() 在主线程上调用,但默认情况下会在后台线程上订阅返回值。您可以替换 RxWorker.getBackgroundScheduler() 来更改订阅线程。

停止 RxWorker 会妥善处理 Observer,因此您无需以任何特殊方式处理停工。

用ListenableWorker处理线程

在某些情况下,您可能需要提供自定义线程处理策略。例如,您可能需要处理基于回调的异步操作。在这种情况下,不能只依靠 Worker 来完成操作,因为它无法以阻塞方式完成这项任务。WorkManager 通过 ListenableWorker 支持该用例。ListenableWorker 是最低层级的工作器 API;WorkerCoroutineWorkerRxWorker 都是从这个类衍生而来的。ListenableWorker 只会发出信号以表明应该开始和停止工作,而线程处理则完全交由您负责完成。开始工作信号在主线程上调用,因此请务必手动转到您选择的后台线程。

抽象方法 ListenableWorker.startWork() 会返回一个将使用操作的 Result 设置的 ListenableFutureListenableFuture 是一个轻量级接口:它是一个 Future,用于提供附加监听器和传播异常的功能。在 startWork 方法中,应该返回 ListenableFuture,完成操作后,您需要使用操作的 Result 设置这个返回结果。您可以通过以下两种方式创建 ListenableFuture

  1. 如果您使用的是 Guava,请使用 ListeningExecutorService
  2. 否则,请将 councurrent-futures 包含到您的 gradle 文件并使用 CallbackToFutureAdapter

如果您希望基于异步回调执行某些工作,可以执行如下操作:

    public class CallbackWorker extends ListenableWorker {

        public CallbackWorker(Context context, WorkerParameters params) {
            super(context, params);
        }

        @NonNull
        @Override
        public ListenableFuture<Result> startWork() {
            return CallbackToFutureAdapter.getFuture(completer -> {
                Callback callback = new Callback() {
                    int successes = 0;

                    @Override
                    public void onFailure(Call call, IOException e) {
                        completer.setException(e);
                    }

                    @Override
                    public void onResponse(Call call, Response response) {
                        ++successes;
                        if (successes == 100) {
                            completer.set(Result.success());
                        }
                    }
                };

                for (int i = 0; i < 100; ++i) {
                    downloadAsynchronously("https://www.google.com", callback);
                }
                return callback;
            });
        }
    }
    

如果您的工作停止会发生什么?如果预计工作会停止,则始终会取消 ListenableWorkerListenableFuture。通过使用 CallbackToFutureAdapter,您只需添加一个取消监听器即可,如下所示:

    public class CallbackWorker extends ListenableWorker {

        public CallbackWorker(Context context, WorkerParameters params) {
            super(context, params);
        }

        @NonNull
        @Override
        public ListenableFuture<Result> startWork() {
            return CallbackToFutureAdapter.getFuture(completer -> {
                Callback callback = new Callback() {
                    int successes = 0;

                    @Override
                    public void onFailure(Call call, IOException e) {
                        completer.setException(e);
                    }

                    @Override
                    public void onResponse(Call call, Response response) {
                        ++successes;
                        if (successes == 100) {
                            completer.set(Result.success());
                        }
                    }
                };

                completer.addCancellationListener(cancelDownloadsRunnable, executor);

                for (int i = 0; i < 100; ++i) {
                    downloadAsynchronously("https://www.google.com", callback);
                }
                return callback;
            });
        }
    }

支持长时间运行的工作器

​ WorkManager 2.3.0-alpha02增加了对长期运行的工作者的一流支持。在这种情况下,WorkManager可以向操作系统提供一个信号,即如果可能的话,在执行该工作的同时,应该保持进程的活力。这些Worker的运行时间可以超过10分钟。这个新功能的用例包括批量上传或下载(不能分组),在本地进行ML模型的压缩,或对应用的用户来说很重要的任务。

​ 在引擎盖下,WorkManager代表你管理并运行一个前台服务来执行WorkRequest,同时也会显示一个可配置的通知。

​ ListenableWorker现在支持setForegroundAsync()API,而CoroutineWorker则支持暂停setForeground()API。这些API允许开发人员指定这个WorkRequest是重要的(从用户的角度来看)或长期运行的WorkRequest。

​ 从 2.3.0-alpha03 版本开始,WorkManager还允许用户创建一个PendingIntent,无需使用createCancelPendingIntent()API注册一个新的Android组件,就可以用来取消Worker。这种方法在与setForegroundAsync()或setForeground()API一起使用时特别有用,可以用来添加一个取消Worker的通知动作。

创建和管理长时间运行的任务
Java

使用ListenableWorker或Worker的开发者可以调用setForegroundAsync()API,返回一个ListenableFuture。你也可以调用setForegroundAsync(),来更新一个正在运行的Notification。

下面是一个简单的例子,它是一个长期运行的worker下载文件。这个Worker会跟踪进度,更新一个正在进行的Notification,显示下载进度。

public class DownloadWorker extends Worker {
    private static final String KEY_INPUT_URL = "KEY_INPUT_URL";
    private static final String KEY_OUTPUT_FILE_NAME = "KEY_OUTPUT_FILE_NAME";

    private NotificationManager notificationManager;

    public DownloadWorker(
        @NonNull Context context,
        @NonNull WorkerParameters parameters) {
            super(context, parameters);
            notificationManager = (NotificationManager)
                context.getSystemService(NOTIFICATION_SERVICE);
    }

    @NonNull
    @Override
    public Result doWork() {
        Data inputData = getInputData();
        String inputUrl = inputData.getString(KEY_INPUT_URL);
        String outputFile = inputData.getString(KEY_OUTPUT_FILE_NAME);
        // Mark the Worker as important
        String progress = "Starting Download";
        setForegroundAsync(createForegroundInfo(progress));
        download(inputUrl, outputFile);
        return Result.success();
    }

    private void download(String inputUrl, String outputFile) {
        // Downloads a file and updates bytes read
        // Calls setForegroundInfoAsync() periodically when it needs to update
       // the ongoing Notification
    }

    @NonNull
    private ForegroundInfo createForegroundInfo(@NonNull String progress) {
        // Build a notification using bytesRead and contentLength

        Context context = getApplicationContext();
        String id = context.getString(R.string.notification_channel_id);
        String title = context.getString(R.string.notification_title);
        String cancel = context.getString(R.string.cancel_download);
        // This PendingIntent can be used to cancel the worker
        PendingIntent intent = WorkManager.getInstance(context)
                .createCancelPendingIntent(getId());

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createChannel();
        }

        Notification notification = new NotificationCompat.Builder(context, id)
                .setContentTitle(title)
                .setTicker(title)
                .setSmallIcon(R.drawable.ic_work_notification)
                .setOngoing(true)
                // Add the cancel action to the notification which can
                // be used to cancel the worker
                .addAction(android.R.drawable.ic_delete, cancel, intent)
                .build();

        return new ForegroundInfo(notification);
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private void createChannel() {
        // Create a Notification channel
    }
}
Kotlin

Kotlin开发者应该使用CoroutineWorker,而不是使用setForegroundAsync(),你可以使用该方法的悬浮版本的setForeground()来代替。

class DownloadWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {

    private val notificationManager =
        context.getSystemService(Context.NOTIFICATION_SERVICE) as
                NotificationManager

    override suspend fun doWork(): Result {
        val inputUrl = inputData.getString(KEY_INPUT_URL)
                       ?: return Result.failure()
        val outputFile = inputData.getString(KEY_OUTPUT_FILE_NAME)
                       ?: return Result.failure()
        // Mark the Worker as important
        val progress = "Starting Download"
        setForeground(createForegroundInfo(progress))
        download(inputUrl, outputFile)
        return Result.success()
    }

    private fun download(inputUrl: String, outputFile: String) {
        // Downloads a file and updates bytes read
        // Calls setForegroundInfo() periodically when it needs to update
        // the ongoing Notification
    }
    // Creates an instance of ForegroundInfo which can be used to update the
    // ongoing notification.
    private fun createForegroundInfo(progress: String): ForegroundInfo {
        val id = applicationContext.getString(R.string.notification_channel_id)
        val title = applicationContext.getString(R.string.notification_title)
        val cancel = applicationContext.getString(R.string.cancel_download)
        // This PendingIntent can be used to cancel the worker
        val intent = WorkManager.getInstance(applicationContext)
                .createCancelPendingIntent(getId())

        // Create a Notification channel if necessary
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createChannel()
        }

        val notification = NotificationCompat.Builder(applicationContext, id)
            .setContentTitle(title)
            .setTicker(title)
            .setContentText(progress)
            .setSmallIcon(R.drawable.ic_work_notification)
            .setOngoing(true)
            // Add the cancel action to the notification which can
            // be used to cancel the worker
            .addAction(android.R.drawable.ic_delete, cancel, intent)
            .build()

        return ForegroundInfo(notification)
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun createChannel() {
        // Create a Notification channel
    }

    companion object {
        const val KEY_INPUT_URL = "KEY_INPUT_URL"
        const val KEY_OUTPUT_FILE_NAME = "KEY_OUTPUT_FILE_NAME"
    }
}

从Firebase JobDispatcher迁移

从GCMNetworkManager迁移

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

10-WorkManager 的相关文章

  • 8021x认证以及portal认证的参考资料

    最近几个月一直在阅读和认证相关的东西 xff0c 到如今 xff0c 也基本上将认证的流程摸得比较清楚了 xff0c 所以在这里写一篇文章 xff0c 记录一下自己的心得 同时也给希望了解这些认证的同学一些参考资料 说实话 xff0c 在没
  • 枚举类返回前端处理

    后端经常使用的枚举类 xff0c 在返回给前段 xff0c 会之间显示枚举名称 xff0c 不是很友好 xff0c 可以通过以下方法解决 枚举类上加 64 JsonFormat 注解 例如以下代码 maven lt dependency g
  • BW:BW与第三方BI接口设计与实现:APD、Open Hub、RFM

    最近公司新上了国内某 CRM系统 xff0c SAP的 CRM也光荣下线了 但是紧接着就出现了一些需求 xff0c CRM自带一款小型 BI xff0c 需要一些 SD的数据 xff0c 但是把 R3的数据给他们进行计算的话 xff0c 不
  • Spring源码解读(一)启动流程分析——AbstractApplicationContext

    前言 通过对Spring启动流程源码的分析 xff0c 能够使我们对spring掌握的更透彻 本篇博文通过5 2 19版本的源码以 new ClassPathXmlApplicationContext 34 classpath spring
  • Spring源码解读(十四)Boot启动类——SpringApplication.run

    前言 继 Spring源码解读 xff08 十三 xff09 Boot启动类 SpringApplication构造方法 后 xff0c 继续分析SpringApplication run方法 程序入口 启动方法中增加一些boot的新特性处
  • 拓扑排序(Topological Sorting)

    一 什么是拓扑排序 在图论中 xff0c 拓扑排序 xff08 Topological Sorting xff09 是一个有向无环图 xff08 DAG Directed Acyclic Graph xff09 的所有顶点的线性序列 且该序
  • Gnome3提取gnome-shell.css以及修改和编译

    原帖 xff1a http hack fdzh org item id 61 522 GNOME 3 12 开始使用 GResource 二进制资源文件保存 js css svg 等文件 xff0c 需使用 gresource 解压 xff
  • docker网络故障排查(重启firewall导致docker启动失败)

    背景 xff1a 生产环境需要用docker发布新的应用 xff0c 结果docker启动失败 xff0c 报了iptables异常 先说导致这个问题的原因 xff1a firewall是基于iptables工具来管理防火墙 xff0c d
  • java内嵌tomcat并启动

    java内嵌tomcat并启动 创建工程 创建maven工程ee tomat embed pom xml添加依赖如下 lt properties gt lt project build sourceEncoding gt UTF 8 lt
  • 20210525:Python学习——re.compile()查找数据是遇到换行

    re compile r 39 lt div class 61 34 sub 34 gt lt div gt 39 re S 被采集的数据中有多行 xff0c 有换行符所以要加re S或者re DOTALL xff0c 换行读取数据 xff
  • 再见,2012,你好,2013.

    其实这篇日志过年前 xff0c 已经写得差不多 xff0c 但是忘记发了 xff0c 现在补上 xff0c 现在应该还不是太晚吧 ps xff1a 想了一个很俗的标题 61 61 2012年 xff0c 是对我意义最不平凡的一年 这一年 x
  • SpringSecurity之CSRF漏洞保护

    1 csrf简介 CSRF Cross Site Request Forgery跨站请求伪造 xff0c 也可称为一键式攻击 one click attack 通常缩写为CSRF或者XSRF CSRF攻击是一种挟持用户在当前已登录的浏览器上
  • GIS开发学习推荐书目

    书名 作者 出版社 版次 GIS 基础 地理信息系统导论 US Kang tsung Chang xff1b 陈健飞 译 电子工业出版社 第七版 ArcGIS地理信息系统空间分析实验教程 汤国安 杨昕 科学出版社 第二版 编程 基础 C语言
  • CSDN 还是不能改头像!!!

    都好几个月了 xff0c 技术问题么 xff0c 还是爬梯开大会 xff0c 连这个都不让改了 真不爽
  • 自己写的HTML与浏览器用截图工具量出来的尺寸不一致

    自己写的HTML与浏览器用截图工具量出来的尺寸不一致border style dashed height 200px width 200px border width 2px border color chartreuse 明显大了50px
  • 网站链接被微信屏蔽禁止访问的解决办法

    最近网站链接被微信屏蔽禁止访问 xff0c 微信内打开网址显示如下提醒 xff1a 已停止访问该网页 xff0c 网页包含诱导分享 关注等诱导行为内容 xff0c 被多人投诉 xff0c 为维护绿色上网环境 xff0c 已停止访问 出现这种
  • 由于找不到vcruntime140_1.dll,无法继续执行代码

    welcome to my blog 问题描述 安装mysql时报错 由于找不到vcruntime140 1 dll 无法继续执行代码 解决方法 这是缺少了动态链接库 ddl文件 跟mysql无关 下载这个ddl文件即可 打开下载地址 找到
  • Ubuntu 18.04 nvidia驱动导致的循环登陆解决

    Ubuntu 18 04 nvidia驱动导致的循环登陆解决 一般情况 无法进入登陆界面 xff1a 建议连接外接显示器看看效果 xff0c 或许有意外的惊喜驱动未安装 显卡未成功切换 自带驱动为屏蔽等 xff1a 请自行尝试百度其它解决方

随机推荐

  • linux下adb连接不上解决方法

    linux下adb连接不上解决方法 参考文章 xff1a xff08 1 xff09 linux下adb连接不上解决方法 xff08 2 xff09 https www cnblogs com asin huang p 8604368 ht
  • 这么学,好玩,有趣,逗死了!(2017年系统规划与管理师上午综合知识试题试题解析(六))

    IT服务风险管理中 xff0c 对风险的识别是很重要的一项工作 识别方法中 xff0c 通常采用文档审查 信息收集技术 检查表 分析假设和图解技术 下面 xff08 xff09 选项不属于信息采集技术 xff08 51 xff09 A 德尔
  • 松滋历史(1)--松滋县建县历史考证

    刘鲋鮈总共4个儿子 xff1a 刘建德 刘梁 刘延年 刘度 长沙顷王刘鲋鮈的儿子刘梁 汉昭帝始元六年 xff08 公元前81年 xff09 xff0c 六月乙未封高城节侯 汉宣帝元康元年 xff08 公元前65年 xff09 xff0c 正
  • 松滋历史(2)--高成县为什么这么高光?

    东汉开国皇帝刘秀 xff0c 是长沙定王刘发第5代世孙 也就是说 xff0c 东汉开国皇帝刘秀爷爷的爷爷与高城侯刘梁 xff08 高城县 xff08 松滋县前身 xff09 创始人 xff09 的爷爷是亲兄弟 在哪个特别讲血统的年代 xff
  • 为什么要报考系统架构设计师考试

    为什么要报考系统架构师考试 最近一年多 xff0c 很多朋友来信 xff0c 问我什么要报考系统架构设计师考试 为什么参加这个考试 xff0c 这个考试有用吗 xff1f 对自己的职业会带来什么好处 xff1f 我想有以下几个方面 xff1
  • 2014年24段魔尺变三叶花视频教程

    2014年24段魔尺变三叶花视频教程 xff08 升级版 xff09 偶是真心喜欢24段魔尺制作的三叶花 xff0c 那是相当漂亮 xff0c 体现了几何美 xff0c 对称美 xff0c 空间美 xff0c 色彩美 xff0c 见下图 三
  • BW:LO数据源初始化步骤(精简版)

    首先在r3上删除 setup table xff0c 然后再填充 setup table xff08 锁定凭证 xff09 xff0c 同时 xff0c 在bw做无数据的初始化 xff0c 最后在bw做full load
  • 24段魔尺,可以折出哪些精美图案

    24段魔尺 xff0c 可以折出哪些精美图案 24段魔尺 xff0c 究竟可以叠成哪些精美图案 xff1f 球 小乌龟 十字架 三叶花 三角形 牛魔王 音符是大家比较熟悉的 偶对这个问题 xff0c 比较感兴趣 先将网络上流传的24段魔尺折
  • 软考证到底有多大个鸟用?

    软考证到底有多大个鸟用 xff1f 小虎以信息系统项目管理师高级证书为例 xff0c 来说说这个证 xff0c 到底有什么用 xff1f 1 求职简历上 xff0c 可以浓彩重抹一笔 xff0c 好好吹嘘下自己 xff0c 是IT高级人才
  • 明朝开国第一重臣李善长长子李祺之长子李芳后裔在松滋

    明朝开国第一重臣李善长长子李祺之长子李芳后裔在松滋 李善长 xff0c 何许人也 肩比汉代丞相萧何 xff1b 功臣里排位第一 xff0c 比刘伯温 徐达 汤和 常遇春 蓝玉 李文忠地位高 xff1b 太师 xff0c 左丞相 xff0c
  • 程序员很少上《非诚勿扰》电视节目相亲之分析

    程序员很少上 非诚勿扰 电视节目相亲之分析 偶观看有线电视32 频道的江苏卫视 非诚勿扰 电视相亲节目 xff0c 半年有余 问题 为什么程序员很少上 非诚勿扰 节目进行相亲呢 xff1f 难道是程序员当中 xff0c 很少有大龄的剩男剩女
  • 2018年CSDN博客排名第一名,花落谁家?

    截止2018年10月9日 https blog csdn net stpeace 小虎问题 xff1a 通过搜索引擎例如 xff1a 百度 搜狗 Bing等 如何快速搜索 xff0c 定位谁是CSDN博客的第几名 xff1f 想知道谁CSD
  • 客家刘开七、刘广传刘氏七律族诗赏析V2.0

    七律 刘氏族诗 南宋 刘广传 骏马骑行各出疆 xff0c 任从随地立纲常 年深外境皆吾境 xff0c 日久他乡即故乡 早晚勿忘亲命语 xff0c 晨昏须顾祖炉香 苍天佑我卯金氏 xff0c 八七男儿共炽昌 一 平仄性赏析 除了 卯 字 xf
  • 系统mysql、sqlserver数据库兼容方案

    一 技术选型 springboot2 4 43 mybatisplus3 4 43 mysql5 7 43 redis3 0 43 二 编写目的 如果产品开发默认数据库采用mysql xff0c 但是当客户提出数据库需要采用Sqlserve
  • eclipse maven plugin 插件 安装 和 配置

    离线插件 点击下载离线安装包 xff1a eclipse maven plugin zip for eclipse helios or higher 解压缩到任意目录 xff08 如这里的plugins目录 xff09 xff1a 目录路径
  • Android 使用View Binding来代替频繁findViewById来得到view

    从 Android Studio 3 6 开始 xff0c 视图绑定能够通过生成绑定对象来替代 findViewById xff0c 从而可以帮您简化代码 移除 bug xff0c 并且从 findViewById 的模版代码中解脱出来 在
  • 李开复辞职前后的故事

    这些故事出自即将发售的 世界因你不同 xff1a 李开复自传 一书 xff0c 由李开复本人和北京青年报记者范海涛合写而成 以下为故事部分节选 xff1a 今天 xff0c 我开得很慢 xff0c 仿佛是让自己的心情在辽阔的天空下能够更加安
  • 哈哈,终于知道CSDN怎么改头像了

    话说之前一直郁闷 xff0c 说改头像的功能还没修好 xff0c 一直说服务器错误 今天偶尔发现 xff0c 右上角有个设置 xff0c 原来在这里可以改 我还发了几封邮件给CSDN的admin xff0c 居然只知道道歉 xff0c 不告
  • tail -f 在串口中查看日志文件

    tail f opt log log mi tail follow opt log log mi 如果想从串口中查看日志文件 用tail 命令就可以了
  • 10-WorkManager

    WorkManager 文章目录 WorkManager概览使用入门将WorkManager添加到项目中创建后台任务配置运行任务的方式和时间将任务提交给系统后续步骤 方法指南定义WorkRequest工作约束初始延迟重试和退避政策定义任务的