后台工作利器-WorkManager(Part2)

209 阅读7分钟

workmanager_main.svg

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

上一篇我们讲了WorkManager三要素中的两个,Worker和Work Request点击回顾。今天我们就来看看最后一个,然后把这三者结合起来学习。

WorkManager

上文我们提到了WorkManager是一个管理者。那么当我们创建了Worker并创建了WorkRequest来配置工作后,那么我就要用WorkManager来管理或观察工作。下面管理观察需要用到的知识点,我们来逐一看看。

  1. 工作状态

    状态是我们管理工作和观察工作的一个重要指标。而在WorkManager中工作状态分别为ENQUEUEDRUNNINGSUCCEEDEDFAILEDRETRYCANELLEDBLOCKED,一共7种。

    • 一次性工作

      一次性工作的最终状态有三个。分别是成功,失败,取消。过程中状态有入列,运行,重试。 image.png

    • 周期性工作

      因为是不停止的周期性工作,所以最终状态只有取消。过程状态有入列,运行,重试,成功,失败。 image.png

    • 链接工作

      链接工作是把多个一次性工作链接起来的工作链。每个单独工作具备一次性工作的状态外,还多一个堵塞状态。

    image.png

    链接工作顾名思义是把多个一次性工作按照我们希望的执行顺序链接起来。只有当父节点的工作返回了状态后他们才会根据父节点状态来改变状态,每个节点返回的状态都会受预设的重试和退避政策影响。如图所示,父节点在ENQUEUED状态时,子节点的任务都是BLOCKED状态。如果这里的根节点返回了FAILED,那么下面的所有节点都会变成FAILED状态,因为链接中的状态是向下传递的。这点在我们设计的时候一定要记住!

  2. 管理工作

    • 工作入列

      定义 Worker和 WorkRequest 后,最后一步是将工作加入队列。将工作加入队列的最简单方法是调用 WorkManager enqueue() 方法,然后传递要运行的 WorkRequest。另外如果我们想要保证工作在工作队列中的唯一性的话我们可以用WorkManager.enqueueUniqueWork()WorkManager.enqueueUniquePeriodicWork()方法分别对一次性工作和周期工作进行入列操作,来保证我们工作在队列中的唯一性。

      这两种方法都接受 3 个参数:

      • uniqueWorkName - 用于唯一标识工作请求的 String
      • existingWorkPolicy - 此 enum 可告知 WorkManager:如果已有使用该名称且尚未完成的唯一工作链,应执行什么操作。如需了解详情,请参阅冲突解决政策
      • work - 要调度的 WorkRequest
    • 取消工作

      // 通过id
      workManager.cancelWorkById(syncWorker.id)
      
      // 通过唯一名字
      workManager.cancelUniqueWork("sync")
      
      //通过标签
      workManager.cancelAllWorkByTag("syncTag")
      

      这里需要注意,如果多个工作有着同一标签,那么会批量取消。如果取消的工作是工作中的(RUNNING状态)那么用ListenableWorker.isStopped()方法验证是否停止,并且可以

  3. 观察工作

    文章中我们自定义的worker类都是继承Worker类或CoroutineWorker。他们其实都是ListenableWorker的子类哈。只不过CoroutineWorker给我们提供了协程的支持,让我们可以在do work里面可以执行挂起函数。ListenableWorker允许我们在工作结束前设置进度(中间进度),这里我用CoroutineWorker来演示如何设置和观察进度。

    • 更新进度

      setProgress方法

      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()
          }
      }
      
    • 查找工作
      // 通过id查找
      workManager.getWorkInfoById(syncWorker.id) // ListenableFuture<WorkInfo>
      
      // 通过唯一名字查找
      workManager.getWorkInfosForUniqueWork("sync") // ListenableFuture<List<WorkInfo>>
      
      // 通过标签查找
      workManager.getWorkInfosByTag("syncTag") // ListenableFuture<List<WorkInfo>>
      
    • 观察对应工作的对应的进度
      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
                  }
          })
      
  4. 链接工作

    WorkManager 可以创建工作链并将其加入队列。工作链用于指定多个依存任务并定义这些任务的运行顺序。当您需要以特定顺序运行多个任务时,此功能尤其有用。

    首个需要执行的工作我们用beginxxx方法处理

    image.png 这里可以传入唯一的一次性工作或者一次性工作集合(注意集合里的工作运行可能是并行),也可以传入非唯一性的工作实例或者实例集合。

    后面需要接着做的工作我们用then方法

    image.png 这里传参跟上面一样哈

    最后我们别忘了调用enqueue方法让工作链入列。

    可能这个时候有xdm要问了,如果后面的工作需要前面工作的输出数据怎么办?好比,我们第一个工作请求服务器拿到首页的数据,然后工作链后面的工作缓存数据后去渲染首页。好在WorkManager设计的时候已经考虑到了这个问题,赋予工作链中上游工作对下游工作的数据传递。我们现在看看代码示例

    新建一个worker类

      //上游工作
      class ParentSyncWorker(appContext: Context, params: WorkerParameters) :
          CoroutineWorker(appContext, params) {
          override suspend fun doWork(): Result {
              withContext(Dispatchers.IO) {
                  Log.d("WW", "doWork: 上游工作模拟耗时操作")
                  Thread.sleep(2000L)
              }
              //success接受一个Data类型的参数,内联函数workDataOf通过Data构建器构建Data数据
              //大家可以把Data类看成一个持有hashmap<String, Any?>对象的包装类哈
              //我们这里传入一个字符串 "hello",待会看看下游工作能否拿到
              return Result.success(workDataOf("PARENT_KEY" to "hello"))
          }
      }
    

    修改一下之前now android里面的worker类

         @HiltWorker
         class SyncWorker @AssistedInject constructor(
             @Assisted private val appContext: Context,
             @Assisted workerParams: WorkerParameters,
             private val niaPreferences: NiaPreferencesDataSource,
             private val topicRepository: TopicsRepository,
             private val newsRepository: NewsRepository,
             @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
         ) : CoroutineWorker(appContext, workerParams), Synchronizer {
    
             override suspend fun getForegroundInfo(): ForegroundInfo =
                 appContext.syncForegroundInfo()
    
             override suspend fun doWork(): Result {
                 return withContext(ioDispatcher) {
                     //我们通过inputData去取上游输出的字符串,并在下面打印出来
                     val string = inputData.getString("PARENT_KEY")
                     Log.d("WW", "doWork: $string XDM")
                     traceAsync("Sync", 0) {
                         // First sync the repositories in parallel
                         val syncedSuccessfully = awaitAll(
                             async { topicRepository.sync() },
                             async { newsRepository.sync() },
                         ).all { it }
    
                         if (syncedSuccessfully) {
                             Result.success()
                         } else {
                             Result.retry()
                         }
                     }
                 }
             }
    

    开始工作链入列操作

     class SyncInitializer : Initializer<Sync> {
         override fun create(context: Context): Sync {
             //构建上游工作的work request
             val build = OneTimeWorkRequestBuilder<ParentSyncWorker>()
                 //选择合并策略
                 //OverwritingInputMerger key相同时覆盖value
                 //ArrayCreatingInputMerger key相同时不覆盖而是返回一元数组
                 //这里我们随便用ArrayCreatingInputMerger做演示
                 .setInputMerger(ArrayCreatingInputMerger::class.java)
                 .build()
    
    
             WorkManager.getInstance(context).apply {
                 // Run sync on app startup and ensure only one sync worker runs at any time
                 //指定起始工作,因为是用的beginUniqueWork方法来保证唯一性所以我们需要三个参数
                 beginUniqueWork(
                     SyncWorkName,
                     ExistingWorkPolicy.KEEP,
                     build,
                 ).then(//指定下游工作,我们这里传入的是nowandroid项目里面的worker
                     SyncWorker.startUpSyncWork(),
                 ).enqueue()//最后不要忘了入列
             }
    
             return Sync
         }
    
         override fun dependencies(): List<Class<out Initializer<*>>> =
             listOf(WorkManagerInitializer::class.java)
     }
    

    最后可以看到日志成功打印出了上游的数据

    image.png

  5. 测试调试工作

    测试这块很抱歉。本人没有任何实际使用经验,所以就不在这班门弄斧了。但是熟掌握了测试功能,对我们写单元测试等是大有益处的,大家有兴趣的可以去官网学习。测试 worker 实现  |  Android 开发者  |  Android Developers (google.cn)

结尾

最近正好在学习Google出的nowandroid项目,旨在通过写文章记录和分享其中的过程,希望能坚持下来作为一个系列和大家一起学习MAD技术。这两篇文章算是番外篇了,主要是WorkManager这个库本身的东西还是有点多,所以决定单独用两章来简单介绍一下这个库,后面我们还是回到nowandroid项目上去接着分享后面的内容。

后台工作利器-WorkManager(Part1) - 掘金 (juejin.cn)

尊重原创,本文部分内容摘自官网

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情