WorkManager 入门 晋级 测试

458 阅读5分钟

WorkManager 是适合用于持久性工作的推荐解决方案。如果工作始终要通过应用重启和系统重新启动来调度,便是持久性的工作。由于大多数后台处理操作都是通过持久性工作完成的,因此 WorkManager 是适用于后台处理操作的主要推荐 API。

持久性工作的类型

  • WorkManager 可处理三种类型的持久性工作:
  • 立即执行:必须立即开始且很快就完成的任务,可以加急。
  • 长时间运行:运行时间可能较长(有可能超过 10 分钟)的任务。
  • 可延期执行:延期开始并且可以定期运行的预定任务。

功能和优势

  • 工作约束

    • 使用工作约束明确定义工作运行的最佳条件。例如,仅在设备采用不按流量计费的网络连接时、当设备处于空闲状态或者有足够的电量时运行。
  • 强大的调度

    • WorkManager 允许您使用灵活的调度窗口调度工作,以运行一次性重复工作。您还可以对工作进行标记或命名,以便调度唯一的、可替换的工作以及监控或取消工作组。
    • 已调度的工作存储在内部托管的 SQLite 数据库中,由 WorkManager 负责确保该工作持续进行,并在设备重新启动后重新调度。
    • 此外,WorkManager 遵循低电耗模式等省电功能和最佳做法,因此您在这方面无需担心。
  • 加急工作

    • 您可以使用 WorkManager 调度需在后台立即执行的工作。您应该使用加急工作来处理对用户来说很重要且会在几分钟内完成的任务。
  • 灵活的重试政策

  • 工作链

    • 对于复杂的相关工作,您可以使用直观的接口将各个工作任务串联起来,这样您便可以控制哪些部分依序运行,哪些部分并行运行。
     //你可以创建一个Worker , 使用beginUniqueWork + enqueue() 执行改Work 。 如果你想多个任务链式执行的话 你可以直接使用beginUniqueWork + then()+then()...+ enqueue() 来执行
     workManager
                .beginUniqueWork(
                    IMAGE_MANIPULATION_WORK_NAME,
                    ExistingWorkPolicy.REPLACE,
                    OneTimeWorkRequest.from(CleanupWorker::class.java)
                ).then(OneTimeWorkRequestBuilder<BlurWorker>().build()).enqueue()
    
  • 使用 WorkManager 保证工作可靠性

    WorkManager 适用于需要可靠运行的工作,即使用户导航离开屏幕、退出应用或重启设备也不影响工作的执行。例如:

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

    WorkManager 不适用于那些可在应用进程结束时安全终止的进程内后台工作。它也并非对所有需要立即执行的工作都适用的通用解决方案。请查看后台处理指南,了解哪种解决方案符合您的需求。

  • 取代已弃用的 API

    WorkManager API 是一个适合用来替换先前的 Android 后台调度 API(包括 FirebaseJobDispatcherGcmNetworkManagerJobScheduler)的推荐组件。 自我感觉没有什么用

WorkManager 使用入门

首先添加相关依赖到项目中

dependencies {
    val work_version = "2.7.1"
     // Kotlin + coroutines
    implementation("androidx.work:work-runtime-ktx:$work_version") 
}
  • 自定义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 work finished successfully with the Result
           return Result.success()
       }
    }

doWork() 返回的 [Result()](ListenableWorker.Result  |  Android Developers)会通知 WorkManager 服务工作是否成功,以及工作失败时是否应重试工作。

Result.success():工作成功完成。

Result.failure():工作失败。

Result.retry():工作失败,应根据其重试政策在其他时间尝试。

  • 创建WorkRequest

定义工作后,必须使用 WorkManager 服务进行调度该工作才能运行。对于如何调度工作,WorkManager 提供了很大的灵活性。您可以将其安排为在某段时间内定期运行,也可以将其安排为仅运行一次

不论您选择以何种方式调度工作,请始终使用 [WorkRequest](https://developer.android.com/reference/androidx/work/WorkRequest)Worker 定义工作单元,[WorkRequest](https://developer.android.com/reference/androidx/work/WorkRequest)(及其子类)则定义工作运行方式和时间。在最简单的情况下,您可以使用 [OneTimeWorkRequest](https://developer.android.com/reference/androidx/work/OneTimeWorkRequest),如以下示例所示。


    // one time work 
   val uploadWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<UploadWorker>()
      // Additional configuration
       .build()

//您的应用有时可能需要定期运行某些工作 例如定期的数据备份或者日志上传

val saveRequest =
       PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
    // Additional configuration
           .build()

//你可以在 显示NetworkType  BatteryNotLow RequiresCharging DeviceIdle StorageNotLow 等约束条件下运行
//以下代码会构建了一个工作请求,该工作请求仅在用户设备正在充电且连接到 Wi-Fi 网络时才会运行:
val constraints = Constraints.Builder()
   .setRequiredNetworkType(NetworkType.UNMETERED)
   .setRequiresCharging(true)
   .build()

val myWorkRequest: WorkRequest =
   OneTimeWorkRequestBuilder<MyWork>()
       .setConstraints(constraints)
       .build()

// 以下是可在每小时的最后 15 分钟内运行的定期工作的示例。你可以灵活的配置这些时间

val myUploadWork = PeriodicWorkRequestBuilder<SaveImageToFileWorker>(
       1, TimeUnit.HOURS, // repeatInterval (the period cycle)
       15, TimeUnit.MINUTES) // flexInterval
    .build()

//重复间隔必须大于或等于 #PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,而灵活间隔必须大于或等于 PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS。
val request = OneTimeWorkRequestBuilder()
//   workManager 从2.7 开始加入了加急任务的处理 你可以通过setExpedited() 来让request 加急处理
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .build()

WorkManager.getInstance(context)
    .enqueue(request)
  • 分配输入数据

    
    /**输入值以键值对的形式存储在 Data 对象中,并且可以在工作请求中设置。
    
    WorkManager 会在执行工作时将输入 Data 传递给工作。
    
    Worker 类可通过调用 Worker.getInputData() 访问输入参数。
    
    以下代码展示了如何创建需要输入数据的 Worker 实例,以及如何在工作请求中发送该实例。**/
    
    // Define the Worker requiring input
    class UploadWork(appContext: Context, workerParams: WorkerParameters)
       : Worker(appContext, workerParams) {
    
       override fun doWork(): Result {
           val imageUriInput =
               inputData.getString("IMAGE_URI") ?: return Result.failure()
    
           uploadFile(imageUriInput)
           return Result.success()
       }
       ...
    }
    
    // Create a WorkRequest for your Worker and sending it input
    val myUploadWork = OneTimeWorkRequestBuilder<UploadWork>()
       .setInputData(workDataOf(
           "IMAGE_URI" to "http://..."
       ))
       .build()
    
  • 提交任务并执行

    WorkManager
        .getInstance(myContext)
        .enqueue(uploadWorkRequest)
    

现在你的request 可以正常的执行了。

// 每个工作请求都有一个唯一标识符,该标识符可用于在以后标识该工作,以便取消工作或观察其进度。
//例如,WorkManager.cancelAllWorkByTag(String) 会取消带有特定标记的所有工作请求,WorkManager.getWorkInfosByTag(String) 会返回一个 WorkInfo 对象列表,该列表可用于确定当前工作状态。
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
   .addTag("cleanup")
   .build()

//可以使用 WorkInfo.getTags() 获取与 WorkRequest 关联的标记集。 
//从 Worker 类中,您可以通过 ListenableWorker.getTags() 检索其标记集。
  • 唯一工作

    唯一工作是一个很实用的概念,可确保同一时刻只有一个具有特定名称的工作实例。与 ID 不同的是,唯一名称是人类可读的,由开发者指定,而不是由 WorkManager 自动生成。与标记不同,唯一名称仅与一个工作实例相关联。

    唯一工作既可用于一次性工作,也可用于定期工作。您可以通过调用以下方法之一创建唯一工作序列,具体取决于您是调度重复工作还是一次性工作。

    1. WorkManager.enqueueUniqueWork()(用于一次性工作)
    2. WorkManager.enqueueUniquePeriodicWork()(用于定期工作)
val sendLogsWorkRequest =
       PeriodicWorkRequestBuilder<SendLogsWorker>(24, TimeUnit.HOURS)
           .setConstraints(Constraints.Builder()
               .setRequiresCharging(true)
               .build()
            )
           .build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
           "sendLogs",
           ExistingPeriodicWorkPolicy.KEEP,
           sendLogsWorkRequest
)

向后兼容性和前台服务

为了保持加急作业的向后兼容性,WorkManager 可能会在 Android 12 之前版本的平台上运行前台服务。前台服务可以向用户显示通知。

在 Android 12 之前,工作器中的 getForegroundInfoAsync()getForegroundInfo() 方法可让 WorkManager 在您调用 setExpedited() 时显示通知。

如果您想要请求任务作为加急作业运行,则所有的 ListenableWorker 都必须实现 getForegroundInfo 方法。

注意如果未能实现对应的 getForegroundInfo 方法**那么在旧版平台上调用 setExpedite时能会导致运行时崩溃。

一次性工作状态

image.png

SUCCEEDEDFAILEDCANCELLED 均表示此工作的终止状态。如果您的工作处于上述任何状态,WorkInfo.State.isFinished() 都将返回 true。

定期工作的状态

image.png

观察你的工作

// by id
workManager.getWorkInfoById(syncWorker.id) // ListenableFuture<WorkInfo>

// by name
workManager.getWorkInfosForUniqueWork("sync") // ListenableFuture<List<WorkInfo>>

// by tag
workManager.getWorkInfosByTag("syncTag") // ListenableFuture<List<WorkInfo>>

该查询会返回 WorkInfo 对象的 ListenableFuture,该值包含工作的 id、其标记、其当前的 State 以及通过 Result.success(outputData) 设置的任何输出数据。

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


workManager.getWorkInfoByIdLiveData(syncWorker.id)
               .observe(viewLifecycleOwner) { workInfo ->
   if(workInfo?.state == WorkInfo.State.SUCCEEDED) {
       Snackbar.make(requireView(),
      R.string.work_completed, Snackbar.LENGTH_SHORT)
           .show()
   }
}

取消和停止工作
// by id
workManager.cancelWorkById(syncWorker.id)

// by name
workManager.cancelUniqueWork("sync")

// by tag
workManager.cancelAllWorkByTag("syncTag")

测试Worker 的实现

WorkManager 提供了用于测试 WorkerListenableWorkerListenableWorker 变体(CoroutineWorkerRxWorker)的 API。

// 待测试的worker
class SleepWorker(context: Context, parameters: WorkerParameters) :
    Worker(context, parameters) {

    override fun doWork(): Result {
        // Sleep on a background thread.
        Thread.sleep(1000)
        return Result.success()
    }
}

如需测试这个 Worker,您可以使用 TestWorkerBuilder。此构建器有助于构建可用于测试业务逻辑的 Worker 实例。


// Kotlin code uses 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
        ).build()

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

TestWorkerBuilder 也可用于设置标记(例如 inputDatarunAttemptCount),以便您单独验证工作器状态。例如,SleepWorker 将休眠时长当作输入数据,而不是工作器中定义的常量数据:


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

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

    companion object {
        const val SLEEP_DURATION = "SLEEP_DURATION"
    }

SleepWorkerTest 中,您可以将该输入数据提供给 TestWorkerBuilder,以满足 SleepWorker 的需求。


// Kotlin code uses 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 1000L)
        ).build()

        val result = worker.doWork()
        assertThat(result, `is`(Result.success()))
    }
}
测试ListenableWorker及其变体

如需测试 ListenableWorker 或其变体(CoroutineWorkerRxWorker),请使用 TestListenableWorkerBuilderTestWorkerBuilderTestListenableWorkerBuilder 的主要区别在于,TestWorkerBuilder 允许指定用来运行 Worker 的后台 Executor,而 TestListenableWorkerBuilder 依赖 ListenableWorker 实现的线程逻辑。

例如,假设我们需要测试类似以下示例的 CoroutineWorker

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

为了测试 SleepWorker,我们首先使用 TestListenableWorkerBuilder 创建一个工作器实例,然后在协程中调用其 doWork 函数。

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

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

    @Test
    fun testSleepWorker() {
        val worker = TestListenableWorkerBuilder<SleepWorker>(context).build()
       // runBlocking 可用作测试的协程构建器,使任何异步执行的代码都转为并行运行。
         runBlocking {
            val result = worker.doWork()
            assertThat(result, `is`(Result.success()))
        }
    }
}

集成测试

添加相关的测试依赖

dependencies {
    val work_version = "2.4.0"
    // optional - Test helpers
    androidTestImplementation("androidx.work:work-testing:$work_version")
}

work-testing 为测试模式提供了一种特殊的 WorkManager 实现,它通过使用 WorkManagerTestInitHelper 来初始化。

work-testing 工件还提供了 SynchronousExecutor,让您可以更轻松地以同步方式编写测试,而无需处理多个线程、锁定或锁存。

初始化

@RunWith(AndroidJUnit4::class)
class BasicInstrumentationTest {
    @Before
    fun setup() {
        val context = InstrumentationRegistry.getTargetContext()
        val config = Configuration.Builder()
            .setMinimumLoggingLevel(Log.DEBUG)
            .setExecutor(SynchronousExecutor())
            .build()

        // Initialize WorkManager for instrumentation tests.
        WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
    }
}
构建测试
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 插桩测试。这里的要点是,在测试模式中测试 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 没有获得任何输入数据时,其预期的 ResultResult.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 的一个实例,可用于模拟初始延迟、满足 ListenableWorker 实例约束的条件,以及 PeriodicWorkRequest 实例的间隔。

测试初始延迟

工作器可以具有初始延迟。如需测试具有 initialDelayEchoWorker,而不必在测试中等待 initialDelay,您可以使用 TestDriver 通过 setInitialDelayMet 将工作请求的初始延迟标记为已满足条件。

@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())
    // Get the TestDriver
    val testDriver = WorkManagerTestInitHelper.getTestDriver()
    // Enqueue
    workManager.enqueue(request).result.get()
    // Tells the WorkManager test framework that initial delays are now met.
    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
    workManager.enqueue(request).result.get()
    // Tells the testing framework that all constraints are met.
    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了吧 如果有任何问题可以大家一起讨论

附录

developer.android.com/topic/libra…

medium.com/androiddeve…

WorkManager: Working in the background - MAD Skills - YouTube