前言
上一次我們對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,推薦你閱讀我之前的文章
最後我們將上面的幾個WorkRequest結合起來執行,看下它們的最終效果:
通過這篇文章,希望你能夠熟悉運用WorkManager。如果這篇文章對你有所幫助,你可以順手點贊、關注一波,這是對我最大的鼓勵!
項目地址
Android精華錄
該庫的目的是結合詳細的Demo來全面解析Android相關的知識點, 幫助讀者能夠更快的掌握與理解所闡述的要點