前言
WorkManager是AndroidJetpack的一个库,当它的执行条件满足时,我们用它来执行可延迟的任务。虽然在处理后台任务方面,google官方博客中发布了很多解决方案,但是出现次数最多的就是Android Jetpack的WorkManager库。它扩展了JobScheduler 框架的API,并且兼容Android4.0+。
接下来我们就探索WorkManager的基本信息,怎么使用它,什么时候使用它,了解其背后的运行机制等
一、WorkManager简介
1.1 WorkManager是什么?
WorkManager是Android Architecture Components组件之一,并且是Jetpack的是部分。是一种新的关于如何构建新型App的手段。
WorkManager可以延迟执行后台任务
WorkManager是针对一些即使App退出了也要由系统确保运行的任务设计的。
换句话说,WorkManager提供了电量友好型API,它封装了数年来Android后台任务限制的演变。这对需要执行后台任务的App来说,是至关重要的。
1.2 WorkManager什么时候使用
当各种限制条件被满足,WorkManager就可以处理需要运行的后台任务了,无论app进程是否存活。 当app处于后台、前台或者从前台变为后台,后台任务都能够被启动。不管App正在做什么,如果Android 系统杀死了改进程,后台任务应该继续执行或者被重启。(TODO 到底什么时候执行后台任务,是否对App进程没有要求)
WorkManager的一个常见混淆是,它用于需要在“后台”线程中运行但不需要在进程终止后继续运行的任务。事实并非如此。对于这个用例,还有其他解决方案,比如Kotlin的协程、线程池或RxJava等库。您可以在后台处理指南中找到关于这个用例的更多信息。(TODO 要替换链接)
有许多不同的需要运行后台任务的情形了,因此有不同的运行方案。如下图所示:
因此,WorkManager对于那些可以延迟但是必须要执行的后台任务来说,是上佳选择。
首先要思考一下问题?
- 这个任务是必须要完成的吗?
如果关闭了App,它需必须要去完成这个任务吗?比如:笔记同步问题。一旦你完成笔记书写,你期望你的App能够同步数据到后端服务器。如果你切换了App或者系统杀死了你的App,你也希望能够同步成功。甚至是你重启了你的设备,你也希望能够如此。WorkManager能完成这个任务 - 这个任务是可以延迟处理的吗?
我们能稍后运行这个任务吗或者说只有立刻运行该任务,它才能有效吗?如果可以稍后运行,那么他就是可以延迟的。回到上一个例子中。笔记实时上传固然很好,但是稍后上传,问题也不大。WorkManager尊周OS后台限制并且以一种电池高效的方式来尽力完成该工作。
因此,WorkManager针对那些可延迟的一定要完成的后台任务而设计的。如果你想以精确的时刻运行某项任务,请使用AlarmManager;如果想立即执行,请是有前台服务。
当你需要在更复杂的场景触发后台任务,你需要WorkManager能和其他API配合使用。
- 如果该任务是有后台服务器发起的,则需要WorkManager配合Firebase Cloud Messaging使用。
- 如果你使用广播接收者监听,然后触发一个耗时任务,你需要使用WrokManager。注意:WorkManager提供了需要参考了广播的约束支持,因此,在某些情形下,你不必注册广播接收者了(TODO 是真的吗????我不信)
1.3 为什么使用WorkManager
WorkManager运行后台任务时,同时会考虑兼容性问题、电池最佳实践和系统健康(好人性啊)。并且,使用WorkManager,你可以定制周期性任务或者复杂的依赖任务。后台任务可以被并行执行或者顺序执行,当你指定运行顺序之后。WorkManager能够在任务间无缝地处理输入输出。
当后台任务应该执行的时候,你也可以设置标准(criteria)。比如:如果没有网络,就没有必要请求远程服务。你可以设置一个约束:只有有网络的使用,才可以运行该任务。
作为保障执行任务的一部分,WorkManager能够跨设备或在应用重启后(TODO 我怎么不信呢)坚持你的任务
最终,WorkManager能让你聚焦于工作请求的状态,从而去更新UI。
总结一下,WorkManager提供了如下的好处:
- 处理不同系统版本的兼容性
- 遵循系统健康的最佳实践
- 支持异步的单次或者周期性任务
- 支持链式任务
- 设置任务执行的约束条件
- 保证任务能够执行,即便app或者设备重启
让我们看一个例子,我们构建了一个并发的实现过滤图片的任务的执行路径。然后结果会被传递给压缩任务,最后再进行上传。
这些任务形成了一条精确的执行链。我们不知道哪一张图片将要被过滤,但是我们知道,压缩任务会在过滤任务完成之后,启动。
1.4 WorkManager怎么制定工作计划的
为了兼容API14,WorkManager选择了一个合适的方式来安排后台任务。它是根据设备的API级别来实现的。WorkManager也许使用JobScheduler或者BroadcastReceiver和AlarmManager实现的。
二、如何使用WorkManager
2.1 准备工作
假设你又一个图片编辑的App,可以过滤、上传图片。你想创建一些列后台任务来实现过滤条件、压缩和上传这些图片。在每一个阶段,它们都有各自的约束条件需要进行校验。具体如下图所示:
使用WorkManager来实现。
增加workmanager依赖
def work_version = "2.4.0"
// (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"
2.2 定义后台任务为"work"
让我们聚焦于你是如何执行一项work,在我们开始多任务之前。我们以上次任务为例。首先要创建一个名为UplaodWorker的类来继承Worker类,并且重写doWork()方法。
- 定义你的任务
- 接收输入,产生输出。并且输入与输出是以键值对的形式展示
- 总是返回一个结果标记,代表成功、失败和重试
class UploadWorker(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {
override fun doWork(): Result {
try {
// Get the input
val imageUriInput = inputData.getString(Constants.KEY_IMAGE_URI)
// Do the work
val response = upload(imageUriInput)
// Create the output of the work
val imageResponse = response.body()
val imgLink = imageResponse.data.link
// workDataOf (part of KTX) converts a list of pairs to a [Data] object.
val outputData = workDataOf(Constants.KEY_IMAGE_URI to imgLink)
return Result.success(outputData)
} catch (e: Exception) {
return Result.failure()
}
}
fun upload(imageUri: String): Response {
TODO(“Webservice request code here”)
// Webservice request code here; note this would need to be run
// synchronously for reasons explained below.
}
}
有两件事需要注意
- 输入、输出是以Data进行传递的,它是一个基本类型或者数组类型的Map。Data 对象传递数据有大小限制。这里被设置为MAX_DATA_BYTES.如果你想要传递更多数据,可以使用Room Database。因此,我传递的是URI,而不是图片本身。
- 在上述代码中,除了Result.success / failure 还有retry方法。
2.3 定义如何执行具体的"work"
当一个worker定义了work所做的内容时,那么一个WorkRequest将定义该work如何和怎么执行。
本例子中使用的是OneTimeWorkRequest 作为UploadWorker的请求,你也可以使用PeriodicWorkRequest。
val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(imageData)
.build()
WorkRequest将imageData作为输入并且尽可能的执行。
接下来我们设置UploadWork只有在有网络的时候可以执行。
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
当然,我们也可以设置其他约束
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.setRequiresStorageNotLow(true)
.setRequiresDeviceIdle(true)
.build()
最后,记住,Result.retry()方法。我之前就提过,如果一个worker返回了retry(),WorkManager将要重新制定执行计划,再次执行该work。你能够自定义退出标准,当你创建一个新的WorkRequest的时候。也就是说,你可以定义什么时候,这个work可以重试。
退出标准,通过两个属性决定:
- BackoffPolicy
- Duration
将上述代码整合(work 、约束条件和自定义退出策略)
// Create the Constraints
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// Define the input
val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)
// Bring it all together by creating the WorkRequest; this also sets the back off criteria
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(imageData)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build()
2.4 运行你的"work"
万事俱备,接下来就是安排workRequest的执行
WorkManager.getInstance().enqueue(uploadWorkRequest)
首先,你需要获得WorkManager的实例(它是一个单例)。调用enqueue方法时🙆WorkManager跟踪、安排当前"work"的整套流程开始启动。
2.5 "work"是如何运行的
那么,你期望WorkManager能够为你做什么呢?WorkManager默认会:
- 在子线程执行你的“work” (前提是你是继承woker,实现其子类)
- 保证你的“work”一定会被执行
- 君子藏器於身,待时而动(根据最佳实践去执行)
记下来,我们看一下,WorkManager是如何保证前两点的。 - Internal TaskExecutor:一个单线程的Executor来实现请求的入队操作。
- WorkManager database:本地数据库,用来记录所有“work”的信息和状态。包括work的状态、输入输出和约束条件。通过持久化到数据库,从而保证App或者设备重启之后,依然能够执行。
- WorkerFactory**:创建worker的默认工程。我们将介绍为什么和如何配置该worker工厂
- Default Executor**:任务真正的执行线程。默认在子线程同步执行。
“**”这些部分能够被重写,从而实现不同的行为。
当你将WorkRequest入队:
- Internal TaskExecutor 立即保存WorkRequest到WorkManager数据库
- 之后,当WorkRequest的约束被满足,Internal TaskExecutor 通知WorkerFactory去创建一个Worker
- 然后,默认的Executor在子线程调用worker的doWork()方法。
因此,能保证worker的任务能够在非UI线程执行。
现在,如果你想要用其他的机制来执行你的work,你可以这么做。它能够支持(CoroutineWorker、RxWorker) 。或者,你能够使用ListenableWorker来精确指定“work”是怎么被执行的。Worker实际上是ListenableWorker的一个具体实现,它是使用默认Executor进行同步请求。因此如果你想要全部控制你的线程策略、异步执行,你可以自定义一个ListenableWorker的子类实现。
虽然WorkManager存储work信息到数据的方式对于保证任务执行是一个绝佳方案。但是,这对于一些不比必须执行的任务或者仅仅是需要在后台执行的任务而言,有些“过犹不及”的味道了。比如,你想要下载一张图片作为View的背景,就不必使用WorkManager。
2.6 使用链实现多任务有序执行
我们的需求是多任务。我们先实现过滤多张图片,然后压缩图片,最后上传。如果你想要(顺序或者并行地)运行一系列的WorkRequest,你可以使用链条(chain)。如下图所示:
上面的需求针对WorkManager来说,是非常简单的。假设你已经使用约束条件创建好了WorkRequests,代码如下所示即可:
WorkManager.getInstance()
.beginWith(Arrays.asList(
filterImageOneWorkRequest,
filterImageTwoWorkRequest,
filterImageThreeWorkRequest))
.then(compressWorkRequest)
.then(uploadWorkRequest)
.enqueue()
三个过滤图片的WorkRequest并行执行。一旦执行完成,compressWorkRequest会去执行,最后是uploadWorkRequest。
链式的另一个特点就是:一个WorkRequest的输出会作为下一个WorkRequest的输入。因此,你需要设置输入、输出正确。
为了处理3个并行WorkRequest的输出,你需要使用InputMerger,尤其是ArrayCreatingInputMerger。代码如下所示:
val compressWorkRequest = OneTimeWorkRequestBuilder<CompressWorker>()
.setInputMerger(ArrayCreatingInputMerger::class.java)
.setConstraints(constraints)
.build()
需要注意的是,InputMerger被添加到了compressWorkRequest实例当中,而不是3个并且的WorkRequest中。
我们知道每一个filterWorkRequest的输出是映射到ImageUri的键KEY_IMAGE_URI。增加ArrayCreatingInputMerger的目的就是 它将并行运行的WorkRequest的输出根据其中的Keys去创建Array。如下图所示:
因此,compressWorkRequest的输入就是以Key是KEY_IMAGE_URI,Value是数组的一个Map对象实例。
2.7 关注WorkRequest的状态
最简单的观察“work”的方式就是使用LiveData。如果你对LiveData不太熟,你可以将它理解为一个可被观察状态的数据持有者对象。
调用getWorkInfoByIdLiveData返回一个WorkInfo的LiveData对象。WorkInfo包含输出数据和工作状态的枚举值。当work执行完成,它的状态是SUCCEEDED。因此,当你想现在屠刀并展示,可以进行如下使用:
WorkManager.getInstance().getWorkInfoByIdLiveData(uploadWorkRequest.id)
.observe(lifecycleOwner, Observer { workInfo ->
// Check if the current work's state is "successfully finished"
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
displayImage(workInfo.outputData.getString(KEY_IMAGE_URI))
}
})
有以下几点需要注意:
- 每一个WorkRequest都有一个唯一的Id,能够根据该值查询WorkInfo
- 当WorkInfo发生变化时,通知观察能力是LiveData赋予的
“Work”通过不同State来表示不同的状态。当观察LiveData< WorkInfo >,你将可以看到这些状态,如下图示:
理想的状态切换应该是:
- BLOCKED:该状态发生在“work”没有在Chain中或者不是Chain中的下一个Work
- ENQUEUED: 只要当前的Work是Chain中的下一个Work(也就是说当前Work是接下来要执行的Work),该Work就会进入该状态。
- RUNNING: 在该状态,Work才真正被执行,也就意味着Worker类方法的doWork方法已经被调用了
- SUCCEEDED:当doWork方法返回Result.success()时,Work进入的最终状态。
但是,RUNNING状态,如果返回Result.failure(),机会进入FAILED状态;同理如果是Result.retry(),则会使Work重新进入ENQUQUED状态。当然,在任何阶段都可以取消,进入CANCELLED状态。整个状态切换如下图所示:
2.8 总结
现在已经对WorkManager API 基本使用介绍完了,你可以:
- 创建一个带有输入、输出的Worker
- 通过WorkRequest、约束、输入来配置你的Worker如何启动
- 将WorkRequest入队
- 理解WorkManager的底层实现(如何使用线程和保证任务执行)
- 通过顺序、并行创建一个复杂的chain,将相关的Work关联起来
- 通过WorkInfo来关注WorkRequest的执行状态。
三、WorkManager满足Kotlin需求
3.1 Kotlin中的WorkManager
本节的代码片使用的是KTX library(KoTlin eXtension). KTX版本的WorkManager提供了许多扩展函数。你可以通过添加依赖,使用这些扩展类和扩展方法。
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
3.2 更加简洁、方便的WorkManager
WorkManager的KTX版本提供了更加友好的创建DataObject方式。比如,在Java中
Data myData = new Data.Builder()
.putInt(KEY_ONE_INT, aInt)
.putIntArray(KEY_ONE_INT_ARRAY, aIntArray)
.putString(KEY_ONE_STRING, aString)
.build();
通过Builder模式创建了Data对象。而在kotlin中,是这么做的:
//KTX 定义了一个内联函数来实现
inline fun workDataOf(vararg pairs: Pair<String, Any?>): Data
//那么若此操作就是比Java简洁好用
val data = workDataOf(
KEY_MY_INT to myIntVar,
KEY_MY_INT_ARRAY to myIntArray,
KEY_MY_STRING to myString
)
3.3 CoroutineWorker
在Java语言中WorkManager中有多个Worker(Worker, ListenableWorker 和 RxWorker),但是Kotlin中只有一个,那就是CoroutineWorker。
Worker类和CoroutineWorker最大的区别是后者的doWork()方法时一个suspend方法。因此CoroutineWorker可以执行异步任务。还有就是,当需要Woker实现onStopped方法时,CoroutineWorker会自动处理停止或者取消。
现在我们就来看一下CoroutineWorker的使用。CoroutineWorker##doWork()方法只是一个suspend函数,也就是说,会在Dispatchers.Default上启动。
class MyWork(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// Do something
Result.success()
} catch (error: Throwable) {
Result.failure()
}
}
}
这也是理解CoroutineWorker不同于其他Worker的一个基础。这也说明,CoroutineWorker##doWork()不运行在WorkManager配置的Executor上。但是我们可以使用withContext()来自定义它的分发器。
class MyWork(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
return try {
// Do something
Result.success()
} catch (error: Throwable) {
Result.failure()
}
}
}
对于你要使用的CoroutineWorker,一般情况下,我们不需要去改变它的Dispatchers。因为大多数情况下,Dispatchers.Default是一个不错的选择的。
3.4 测试WorkManager
WorkManager库也提供了一些依赖来测试你的定义的WorkManager。测试的最初目的是给你的WorkManager提供一个同步的mock环境。你可以使用WorkManagerTestInitHelper#getTestDriver()来模拟延迟和测试周期性任务。
这里的关键点在于你正在更改的你的行为或者你正在更改你的WorkManager来使得你的Work类更容易测试。
WorkManager2.1增加了一个新的方法TestListenableWorkerBuilder,它是一种测量Work类的新的方式。这对于CoroutineWorker老说,是一次非常重要的更新。因为你能够直接使用TestListenableWorkerBuilder来运行你的测试逻辑。
@RunWith(JUnit4::class)
class MyWorkTest {
private lateinit var context: Context
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun testMyWork() {
// Get the ListenableWorker
val worker =
TestListenableWorkerBuilder<MyWork>(context).build()
// Run the worker synchronously
val result = worker.startWork().get()
assertThat(result, `is`(Result.success()))
}
}
这里的关键点就是CoroutineWorker的结果是同步获取的,可以直接看出CoroutineWorker的业务逻辑是否正确。使用TestListenableWorkerBuilder,你可以设置输入参数和runAttemptCount(执行测试)来检验重试逻辑是否正确。下面以“图片上传"逻辑为例:
class MyWork(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val serverUrl = inputData.getString("SERVER_URL")
return try {
// Do something with the URL
Result.success()
} catch (error: TitleRefreshError) {
if (runAttemptCount <3) {
Result.retry()
} else {
Result.failure()
}
}
}
}
你可以使用TestListenableWorkerBuilder测试重试逻辑:
@Test
fun testMyWorkRetry() {
val data = workDataOf("SERVER_URL" to "http://fake.url")
// Get the ListenableWorker with a RunAttemptCount of 2
val worker = TestListenableWorkerBuilder<MyWork>(context)
.setInputData(data)
.setRunAttemptCount(2)
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result, `is`(Result.retry()))
}
@Test
fun testMyWorkFailure() {
val data = workDataOf("SERVER_URL" to "http://fake.url")
// Get the ListenableWorker with a RunAttemptCount of 3
val worker = TestListenableWorkerBuilder<MyWork>(context)
.setInputData(data)
.setRunAttemptCount(3)
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result, `is`(Result.failure()))
}
3.5 总结
随着WorkManager v2.1版本发布,CoroutineWorker变得更加友好。如果你还没有使用过CoroutineWorker,我强烈推荐你尝试一下。
四、周期性的WorkManager
4.1 重复性的Work
在上面,我们已经使用OneTimeWorkRequest来执行Work请求。但是如果要执行周期性Work,那么你需要使用PeriodicWorkRequest。
首先,这两种WorkRequest有什么不同:
- 最小的周期长度是15分钟(这点和JobScheduler相同)
- 在PeriodicWorkRequest不能被放在Chain中使用
- 在2.1版本之前,不能通过delay创建PeriodicWorkRequest
接下来本节会介绍PeriodicWork的使用方式和常见的问题,以及如何书写测试用例。
4.2 API
PeriodicWorkRequest构建方式是不同于之前的OneTimeWorkRequest的。我们需要有一个参数来之处最小的重复时间间隔:
val work = PeriodicWorkRequestBuilder<MyWorker>(1, TimeUnit.HOURS)
.build()
最小时间间隔是需要考虑电池优化和你添加的约束。比如,你指定Work执行知识在充电情况下,即使你的最小时间间隔已经过去了,如果设备没有处于充电,这个Work将不会执行除非你的设备在充电。
在这种情况下,我们添加充电约束到PeriodicWorkRequest并且将其入队:
val constraints = Constraints.Builder()
.setRequiresCharging(true)
.build()
val work = PeriodicWorkRequestBuilder<MyWorker>(1, TimeUnit.HOURS)
.setConstraints(constraints)
.build()
val workManager = WorkManager.getInstance(context)
workManager.enqueuePeriodicWork(work)
注意一下如何获取WorkManager实例
在WorkManager v2.1上WorkManager#getInstance()已经过时,但是引入了一个新的方法WorkManager#getInstance(context:Context)。并且新方法支持按需初始化。本节中,就使用该API来获取WorkManager实例
关于最小时间间隔,需要有一个注意点。WorkManager会平衡不同的需求:Application的WorkRequest和Android操作系统的电量消耗情况。出于这个原因,即使所有的WorkRequest被满足了,你的Work也许会再被延迟一点执行。
Android系统引入了电池优化策略:当用户不使用设备,操作系统会减少活动数目来做电量优化。如果你的设备处于DOZE模式,这就会影响你的Work延迟执行。
4.3 时间间隔和弹性时间间隔
正如上面所示,WorkManager的执行并不是以固定的时间间隔执行。如果有这需求,那么你就看错API了。考虑到给定的时间间隔是最小时间间隔,WorkManager有一个你可以用来指定执行你的Work的window的参数。
简而言之,您可以指定第二个间隔来控制何时允许周期性Worker在时间间隔的一段时间内执行。第二个间隔(flexInterval)位于重复间隔本身的末尾。
看下面的示例。假设你创建了一个周期性的WorkRequest,设置它的时间间隔是30分钟。你可以指定一个flexInterval (该值小于30分钟).
val logBuilder = PeriodicWorkRequestBuilder<MyWorker>(
30, TimeUnit.MINUTES,
15, TimeUnit.MINUTES)
上面的Worker的执行时间如下图所示:
记住:Work的执行时间受到WorkRequest和设备状态的约束。
4.4 以一天为周期的任务
由于我们周期性的时间间隔并不是精确的,所以你不能创建一个在每天的某个精确时刻的WorkRequest,就算是放宽精度也是做的不到这一点。
你可以指定一个24小时的WorkRequest的,但是该Request的执行可能是第一天5:00 am , 第二天5:15 am ,第三天5:17 am ,… 随着时间 误差一直在叠加。在每天都精确执行的任务,是一个常见的需求。
因此,如果你需要每天在大致相同的一个时刻执行任务,你可以使用OneTimeWorkRequest,并且为其设置一个延迟。
This is my new defaultval currentDate = Calendar.getInstance()
val dueDate = Calendar.getInstance()
// Set Execution around 05:00:00 AM
dueDate.set(Calendar.HOUR_OF_DAY, 5)
dueDate.set(Calendar.MINUTE, 0)
dueDate.set(Calendar.SECOND, 0)
if (dueDate.before(currentDate)) {
dueDate.add(Calendar.HOUR_OF_DAY, 24)
}
val timeDiff = dueDate.timeInMillis — currentDate.timeInMillis
val dailyWorkRequest = OneTimeWorkRequestBuilder<DailyWorker>
.setConstraints(constraints) .setInitialDelay(timeDiff, TimeUnit.MILLISECONDS)
.addTag(TAG_OUTPUT) .build()
WorkManager.getInstance(context).enqueue(dailyWorkRequest)
当该次任务执行后,我们需要让下一次的任务入队:
class DailyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
val currentDate = Calendar.getInstance()
val dueDate = Calendar.getInstance()
// Set Execution around 05:00:00 AM
dueDate.set(Calendar.HOUR_OF_DAY, 5)
dueDate.set(Calendar.MINUTE, 0)
dueDate.set(Calendar.SECOND, 0)
if (dueDate.before(currentDate)) {
dueDate.add(Calendar.HOUR_OF_DAY, 24)
}
val timeDiff = dueDate.timeInMillis — currentDate.timeInMillis
val dailyWorkRequest = OneTimeWorkRequestBuilder<DailyWorker>()
.setInitialDelay(timeDiff, TimeUnit.MILLISECONDS)
.addTag(TAG_OUTPUT)
.build()
WorkManager.getInstance(applicationContext)
.enqueue(dailyWorkRequest)
return Result.success()
}
}
请记住 Work的执行始终是要考虑 你设置的约束条件和当前的设备状态。
4.5 周期性“work”的状态
周期性Work的状态和OneTimeWorkRequest是不同的。
如上图所示,周期性Work使用不会有SUCCEEDED状态。除非使用cancel将其退出。
出于此种原因,你不能将周期性Work放入Chain中。
4.6 数据的输入和输入
在输入、输出方面,周期性的Work和OneTimeWorkRequest是有所不同。在OneTimeWorkRequest中,数据对象的输出是通过Result.success()和Result.failure()返回的。而周期性的Work是没有SCUESS状态,来结束任务的。但是我们可以通过ENQUQUED状态监听WorkInfo,来实现数据的获取。
val myPeriodicWorkRequest =
PeriodicWorkRequestBuilder<MyPeriodicWorker>(1, TimeUnit.HOURS).build()
WorkManager.getInstance(context).enqueue(myPeriodicWorkRequest)
WorkManager.getInstance()
.getWorkInfoByIdLiveData(myPeriodicWorkRequest.id)
.observe(lifecycleOwner, Observer { workInfo ->
if ((workInfo != null) &&
(workInfo.state == WorkInfo.State.ENQUEEDED)) {
val myOutputData = workInfo.outputData.getString(KEY_MY_DATA)
}
})
这也许不是一个好的获取结果的方式。你还可以使用其他媒介写入、获取结果,比如数据库的表。
4.7 Work的唯一性
对于线性WorkRequest老说,如果重复执行,仅重复执行完成无可厚非,毕竟线性的WorkRequest是有终点的。但是对于周期性的WorkRequest来说,如果重复设置了多个相同的请求,那么这样的资源浪费就会很严重。面对这样的问题,WorkManager库提供了一个API
—WorkManager#enqueueUniquePeriodicWork(),可以避免周期性Work重复执行。
class MyApplication: Application() {
override fun onCreate() {
super.onCreate()
val myWork = PeriodicWorkRequestBuilder<MyWorker>(
1, TimeUnit.HOURS)
.build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
“MyUniqueWorkName”,
ExistingPeriodicWorkPolicy.KEEP,
myWork)
}
}
该方法避免你重复入队同一Request
4.7 请求策略:是保持还是替换
你选择的测量是由你当WorkRequest的状态。通常默认测量是KEEP。因为犹如你想要替换的WorkRequest正在执行,那么这个替换的代价是比较大的(先取消正在运行的WorkRequest)。
4.8 测试周期性Work
在WorkManager v2.1版本之后,有两种方式测试你的Worker:
- WorkManagerTestInitHelper
- TestWorkBuilder 和 TestListenableWorkBuilder
你可以使用这些工具类来测试任何种类的Worker类。接下来,你看一下用这些构造器来创建测试用例的例子。
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.WorkManager
import androidx.work.testing.TestListenableWorkerBuilder
import com.google.samples.apps.sunflower.workers.SeedDatabaseWorker
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
private lateinit var context: Context
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun testRefreshMainDataWork() {
// Get the ListenableWorker
val worker = TestListenableWorkerBuilder<SeedDatabaseWorker>(context).build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result, `is`(Result.success()))
}
}