WorkManager從入門到實踐,有這一篇就夠了

前言

上一次我們對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的使用,我將其分爲以下幾步:

  1. 構建Work
  2. 配置WorkRequest
  3. 添加到WorkContinuation中
  4. 獲取響應結果

下面我們來通過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類型,可用值有三個

  1. Result.success(): 成功
  2. Result.failure(): 失敗
  3. 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種類包括:

  1. OneTimeWorkRequest
  2. 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相同時的表現,它三個值,分別爲:

  1. REPLACE: 當有相同name且未完成的鏈式請求時,將原來的進度取消並刪除,重新加入新的鏈式請求
  2. KEEP: 當有相同name且未完成的鏈式請求時,鏈式請求保持不變
  3. 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支持兩種方式來獲取響應結果

  1. Request.id: WorkRequest的id
  2. 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結合起來執行,看下它們的最終效果:

clipboard.png

通過這篇文章,希望你能夠熟悉運用WorkManager。如果這篇文章對你有所幫助,你可以順手點贊、關注一波,這是對我最大的鼓勵!

項目地址

Android精華錄

該庫的目的是結合詳細的Demo來全面解析Android相關的知識點, 幫助讀者能夠更快的掌握與理解所闡述的要點

Android精華錄

blog

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章