前言
上一次我们对 Paging 的应用进行了一次全面的分析,这一次我们来聊聊 WorkManager。
如果你对 Paging 还未了解,推荐阅读这篇文章:
Paging 在 RecyclerView 中的应用,有这一篇就够了
本来这一篇文章上周就能够发布出来,但我写文章有一个特点,都会结合具体的 Demo 来进行阐述,而 WorkManager 的 Demo 早就完成了,只是要结合文章一起阐述实在需要时间,上周自身原因也就延期了,想想还是写代码容易啊 …????????
哎呀不多说了,进入正题!
WorkManager
WorkManager 是什么?官方给的解释是:它对可延期任务操作非常简单,同时稳定性非常强,对于异步任务,即使 App 退出运行或者设备重启,它都能够很好的保证任务的顺利执行。
所以关键点是简单与稳定性。
对于平常的使用,如果一个后台任务在执行的过程中,app 突然退出或者手机断网,这时后台任务将直接终止。
典型的场景是:App 的关注功能。如果用户在弱网的情况下点击关注按钮,此时用户由于某种原因马上退出了 App,但关注的请求并没有成功发送给服务端,那么下次用户再进入时,拿到的还是之前未关注的状态信息。这就产生了操作上的 bug,降低了用户的体验,增加了用户不必要的操作。
那么该如何解决呢?很简单,看 WorkManager 的定义,使用 WorkManager 就可以轻松解决。这里就不再拓展实现代码了,只要你继续看完这篇文章,你就能轻松实现。
当然你不使用 WorkManager 也能实现,这就涉及到它的另一个好处:简单。如果你不使用 WorkManager,你就要对不同 API 版本进行区分。
JobScheduler
val service = ComponentName(this, MyJobService::class.java)
val mJobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val builder = JobInfo.Builder(jobId, serviceComponent)
.setRequiredNetworkType(jobInfoNetworkType)
.setRequiresCharging(false)
.setRequiresDeviceIdle(false)
.setExtras(extras).build()
mJobScheduler.schedule(jobInfo)
通过 JobScheduler 来创建一个 Job,一旦所设的条件达到,就会执行该 Job。但 JobScheduler 是在 API21 加入的,同时在 API21&22 有一个系统 Bug
这就意味着它只能用在 API23 及以上的版本
if (Build.VERSION.SDK_INT >= 23) {// use JobScheduler}
既然只能 API23 及以上才能使用 JobScheduler,那么在 API23 以下又该如何呢?
AlarmManager & BroadcastReceiver
这时对于 API23 以下,可以使用 AlarmManager 来进行任务的执行,同时结合 BoradcastReceiver 来进行任务的条件监听,例如网络的连接状态、设备的启动等。
看到这里是不是开始头大了呢,我们开始的目的只是想做一个稳定性的后台任务,最后发现居然还要进行版本兼容。兼容性与实现性进一步加大。
那么有没有统一的实现方式呢?当然有,它就是 WorkManager,它的核心原理使用的就是上面所分析的结合体。
他会结合版本自动使用最佳的实现方式,同时还会提供额外的便利操作,例如状态监听、链式请求等等。
WorkManager 的使用,我将其分为以下几步:
- 构建 Work
- 配置 WorkRequest
- 添加到 WorkContinuation 中
- 获取响应结果
下面我们来通过 Demo 逐步了解。
构建 Work
WorkManager 每一个任务都是由 Work 构成,所以 Work 是任务具体执行的核心所在。既然是核心所在,你可能会认为它会非常难实现,但恰恰相反,它的实现非常简单,你只需实现它的 doWork 方法即可。例如我们来实现一个清除相关目录下的.png 图片的 Work
class CleanUpWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {override fun doWork(): Result {val outputDir = File(applicationContext.filesDir, Constants.OUTPUT_PATH)
if (outputDir.exists()) {val fileLists = outputDir.listFiles()
for (file in fileLists) {
val fileName = file.name
if (!TextUtils.isEmpty(fileName) && fileName.endsWith(".png")) {file.delete()
}
}
}
return Result.success()}
}
所有代码都在 doWork 中,实现逻辑也非常简单:找到相关目录,然后逐一判断目录中的文件是否为.png 图片,如果是就删除。
以上是逻辑代码,关键点是返回值 Result.success(),它是一个 Result 类型,可用值有三个
- Result.success(): 成功
- Result.failure(): 失败
- Result.retry(): 重试
对于 success 与 failure,它还支持传递 Data 类型的值,Data 内部是一个 Map 来管理的,所以对于 kotlin 可以直接使用 workDataOf
return Result.success(workDataOf(Constants.KEY_IMAGE_URI to outputFileUri.toString()))
它传递的值将放入 OutputData 中,可以在链式请求中传递,与最终的响应结果获取。其实本质是 WorkManager 结合了 Room,将数据保存在数据库中。
这一步要点就是这么多,下面进入下一步。
配置 WorkRequest
WorkManager 主要是通过 WorkRequest 来配置任务的,而它的 WorkRequest 种类包括:
- OneTimeWorkRequest
- PeriodicWorkRequest
OneTimeWorkRequest
首先 OneTimeWorkRequest 是作用于一次性任务,即任务只执行一次,一旦执行完就自动结束。它的构建也非常简单:
val cleanUpRequest = OneTimeWorkRequestBuilder<CleanUpWorker>().build()
这样就配置了与 CleanUpWorker 相关的 WorkRequest,而且是一次性的。
在配置 WorkRequest 的过程中我们还可以对其添加别的配置,例如添加 tag、传入 inputData 与添加 constraint 约束条件等等。
val constraint = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val blurRequest = OneTimeWorkRequestBuilder<BlurImageWorker>()
.setInputData(workDataOf(Constants.KEY_IMAGE_RES_ID to R.drawable.yaodaoji))
.addTag(Constants.TAG_BLUR_IMAGE)
.setConstraints(constraint)
.build()
添加 tag 是为了打上标签,以便后续获取结果;传入的 inputData 可以在 BlurImageWork 中获取传入的值;添加网络连接 constraint 约束条件,代表只有在网络连接的状态下才会触发该 WorkRequest。
而 BlurImageWork 的核心代码如下:
override suspend fun doWork(): Result {val resId = inputData.getInt(Constants.KEY_IMAGE_RES_ID, -1)
if (resId != -1) {val bitmap = BitmapFactory.decodeResource(applicationContext.resources, resId)
val outputBitmap = apply(bitmap)
val outputFileUri = writeToFile(outputBitmap)
return Result.success(workDataOf(Constants.KEY_IMAGE_URI to outputFileUri.toString()))
}
return Result.failure()}
在 doWork 中,通过 InputData 来获取上述 blurRequest 中传入的 InputData 数据。然后通过 apply 来处理图片,最后使用 writeToFile 写入到本地文件中,并返回路径。
由于篇幅有限,这里就不一一展开,感兴趣的可以查看源码
PeriodicWorkRequest
PeriodicWorkRequest 是可以周期性的执行任务,它的使用方式与配置和 OneTimeWorkRequest 一致。
val constraint = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// at least 15 minutes
mPeriodicRequest = PeriodicWorkRequestBuilder<DataSourceWorker>(15, TimeUnit.MINUTES)
.setConstraints(constraint)
.addTag(Constants.TAG_DATA_SOURCE)
.build()
不过需要注意的是:它的周期间隔最少为 15 分钟。
添加到 WorkContinuation 中
上面我们已经将 WorkRequest 配置好了,剩下要做的是将其加入到 work 工作链中进行执行。
对于单个的 WorkRequest,可以直接通过 WorkManager 的 enqueue 方法
private val mWorkManager: WorkManager = WorkManager.getInstance(application)
mWorkManager.enqueue(cleanUpRequest)
如果你想使用链式工作,只需调用 beginWith 或者 beginUniqueWork 方法即可。其实它们本质都是实例化了一个 WorkContinuationImpl,只是调用了不同的构造方法。而最终的构造方法为:
WorkContinuationImpl(@NonNull WorkManagerImpl workManagerImpl,
String name,
ExistingWorkPolicy existingWorkPolicy,
@NonNull List<? extends WorkRequest> work,
@Nullable List<WorkContinuationImpl> parents) {}
其中 beginWith 方法只需传入 WorkRequest
val workContinuation = mWorkManager.beginWith(cleanUpWork)
beginUniqueWork 允许我们创建一个独一无二的链式请求。使用也很简单:
val workContinuation = mWorkManager.beginUniqueWork(Constants.IMAGE_UNIQUE_WORK, ExistingWorkPolicy.REPLACE, cleanUpWork)
其中第一个参数是设置该链式请求的 name;第二个参数 ExistingWorkPolicy 是设置 name 相同时的表现,它三个值,分别为:
- REPLACE: 当有相同 name 且未完成的链式请求时,将原来的进度取消并删除,重新加入新的链式请求
- KEEP: 当有相同 name 且未完成的链式请求时,链式请求保持不变
- APPEND: 当有相同 name 且未完成的链式请求时,将新的链式请求追加到原来的子队列中,即当原来的链式请求全部执行后才开始执行。
而不管是 beginWith 还是 beginUniqueWork,它都会返回 WorkContinuation 对象,通过该对象我们可以将后续任务加入到链式请求中。例如将上面的 cleanUpRequest(清除)、blurRequest(图片模糊处理)与 saveRequest(保存)串行起来执行,实现如下:
val cleanUpRequest = OneTimeWorkRequestBuilder<CleanUpWorker>().build()
val workContinuation = mWorkManager.beginUniqueWork(Constants.IMAGE_UNIQUE_WORK, ExistingWorkPolicy.REPLACE, cleanUpRequest)
val blurRequest = OneTimeWorkRequestBuilder<BlurImageWorker>()
.setInputData(workDataOf(Constants.KEY_IMAGE_RES_ID to R.drawable.yaodaoji))
.addTag(Constants.TAG_BLUR_IMAGE)
.build()
val saveRequest = OneTimeWorkRequestBuilder<SaveImageToMediaWorker>()
.addTag(Constants.TAG_SAVE_IMAGE)
.build()
workContinuation.then(blurRequest)
.then(saveRequest)
.enqueue()
除了串行执行,还支持并行。例如将 cleanUpRequest 与 blurRequest 并行处理,完成之后再与 saveRequest 串行
val left = mWorkManager.beginWith(cleanUpRequest)
val right = mWorkManager.beginWith(blurRequest)
WorkContinuation.combine(arrayListOf(left, right))
.then(saveRequest)
.enqueue()
需要注意的是:如果你的 WorkRequest 是 PeriodicWorkRequest 类型,那么它不支持建立链式请求,这一点需要注意了。简单的理解,周期性的任务原则上是没有终止的,是个闭环,也就不存在所谓的链了。
获取响应结果
这就到最后一步了,获取响应结果 WorkInfo。WorkManager 支持两种方式来获取响应结果
- Request.id: WorkRequest 的 id
- Tag.name: WorkRequest 中设置的 tag
同时返回的 WorkInfo 还支持 LiveData 数据格式。
例如,现在我们要监听上述 blurRequest 与 saveRequest 的状态,使用 tag 来获取:
// ViewModel
internal val blurWorkInfo: LiveData<List<WorkInfo>>
get() = mWorkManager.getWorkInfosByTagLiveData(Constants.TAG_BLUR_IMAGE)
internal val saveWorkInfo: LiveData<List<WorkInfo>>
get() = mWorkManager.getWorkInfosByTagLiveData(Constants.TAG_SAVE_IMAGE)
// Activity
private fun addObserver() {
vm.blurWorkInfo.observe(this, Observer {if (it == null || it.isEmpty()) return@Observer
with(it[0]) {if (!state.isFinished) {vm.processEnable.value = false} else {
vm.processEnable.value = true
val uri = outputData.getString(Constants.KEY_IMAGE_URI)
if (!TextUtils.isEmpty(uri)) {vm.blurUri.value = Uri.parse(uri)
}
}
}
})
vm.saveWorkInfo.observe(this, Observer {
saveImageUri = ""
if (it == null || it.isEmpty()) return@Observer
with(it[0]) {saveImageUri = outputData.getString(Constants.KEY_SHOW_IMAGE_URI)
vm.showImageEnable.value = state.isFinished && !TextUtils.isEmpty(saveImageUri)
}
})
......
......
}
再来看一个通过 id 获取的:
// ViewModel
internal val dataSourceInfo: MediatorLiveData<WorkInfo> = MediatorLiveData()
private fun addSource() {val periodicWorkInfo = mWorkManager.getWorkInfoByIdLiveData(mPeriodicRequest.id)
dataSourceInfo.addSource(periodicWorkInfo) {dataSourceInfo.value = it}
}
// Activity
private fun addObserver() {
vm.dataSourceInfo.observe(this, Observer {if (it == null) return@Observer
with(it) {if (state == WorkInfo.State.ENQUEUED) {val result = outputData.getString(Constants.KEY_DATA_SOURCE)
if (!TextUtils.isEmpty(result)) {Toast.makeText(this@OtherWorkerActivity, result, Toast.LENGTH_LONG).show()}
}
}
})
}
结合 LiveData 使用是不是很简单呢?WorkInfo 获取的本质是通过操作 Room 数据库来获取。在文章的 Work 部分已经提到,在执行完 Work 任务之后传递的数据将会保存到 Room 数据库中。
所以 WorkManager 与 AAC 的结合度非常高,目的也是致力于为我们开发者提供一套完整的框架,同时也说明 Google 对 AAC 框架的重视。
如果你还未了解 AAC,推荐你阅读我之前的文章
Room
LiveData
Lifecycle
ViewModel
最后我们将上面的几个 WorkRequest 结合起来执行,看下它们的最终效果:
通过这篇文章,希望你能够熟悉运用 WorkManager。如果这篇文章对你有所帮助,你可以顺手点赞、关注一波,这是对我最大的鼓励!
项目地址
Android 精华录
该库的目的是结合详细的 Demo 来全面解析 Android 相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点
Android 精华录
blog