天天看點

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

Rouse

讀完需要

15

分鐘

速讀僅需8分鐘

1

前言

上一次我們對Paging的應用進行了一次全面的分析,這一次我們來聊聊WorkManager。

如果你對Paging還未了解,推薦閱讀這篇文章:

Paging在RecyclerView中的應用,有這一篇就夠了

本來這一篇文章上周就能夠釋出出來,但我寫文章有一個特點,都會結合具體的Demo來進行闡述,而WorkManager的Demo早就完成了,隻是要結合文章一起闡述實在需要時間,上周自身原因也就延期了,想想還是寫代碼容易啊...??

哎呀不多說了,進入正題!

2

WorkManager

WorkManager是什麼?官方給的解釋是:它對可延期任務操作非常簡單,同時穩定性非常強,對于異步任務,即使App退出運作或者裝置重新開機,它都能夠很好的保證任務的順利執行。

是以關鍵點是簡單與穩定性。

對于平常的使用,如果一個背景任務在執行的過程中,app突然退出或者手機斷網,這時背景任務将直接終止。

典型的場景是:App的關注功能。如果使用者在弱網的情況下點選關注按鈕,此時使用者由于某種原因馬上退出了App,但關注的請求并沒有成功發送給服務端,那麼下次使用者再進入時,拿到的還是之前未關注的狀态資訊。這就産生了操作上的bug,降低了使用者的體驗,增加了使用者不必要的操作。

那麼該如何解決呢?很簡單,看WorkManager的定義,使用WorkManager就可以輕松解決。這裡就不再拓展實作代碼了,隻要你繼續看完這篇文章,你就能輕松實作。

當然你不使用WorkManager也能實作,這就涉及到它的另一個好處:簡單。如果你不使用WorkManager,你就要對不同API版本進行區分。

2.1

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

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

這就意味着它隻能用在API23及以上的版本

複制

if (Build.VERSION.SDK_INT >= 23) {
    // use JobScheduler
}           

複制

既然隻能API23及以上才能使用JobScheduler,那麼在API23以下又該如何呢?

2.2

AlarmManager & BroadcastReceiver

這時對于API23以下,可以使用AlarmManager來進行任務的執行,同時結合BoradcastReceiver來進行任務的條件監聽,例如網絡的連接配接狀态、裝置的啟動等。

看到這裡是不是開始頭大了呢,我們開始的目的隻是想做一個穩定性的背景任務,最後發現居然還要進行版本相容。相容性與實作性進一步加大。

那麼有沒有統一的實作方式呢?當然有,它就是WorkManager,它的核心原理使用的就是上面所分析的結合體。

他會結合版本自動使用最佳的實作方式,同時還會提供額外的便利操作,例如狀态監聽、鍊式請求等等。

WorkManager的使用,我将其分為以下幾步:

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

下面我們來通過Demo逐漸了解。

3

建構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,将資料儲存在資料庫中。

這一步要點就是這麼多,下面進入下一步。

4

配置WorkRequest

WorkManager主要是通過WorkRequest來配置任務的,而它的WorkRequest種類包括:

  1. OneTimeWorkRequest
  2. PeriodicWorkRequest

4.1

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寫入到本地檔案中,并傳回路徑。

由于篇幅有限,這裡就不一一展開,感興趣的可以檢視源碼

4.2

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分鐘。

5

添加到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類型,那麼它不支援建立鍊式請求,這一點需要注意了。簡單的了解,周期性的任務原則上是沒有終止的,是個閉環,也就不存在所謂的鍊了。

6

擷取響應結果

這就到最後一步了,擷取響應結果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結合起來執行,看下它們的最終效果:

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

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

7

項目位址

7.1

Android精華錄

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

連結位址(或者點選閱讀原文):

https://github.com/idisfkj/android-api-analysis