WorkManager 是官方推薦用來在背景處理 persistent 工作的 API。所謂的 persistent 工作指的是,即使 app 重啟或是 device 重啟,仍然需要安排處理的工作。本文章將介紹如何利用 WorkManager 來排程工作。
Table of Contents
Background Works
Android 提供數種方式讓開發者處理背景工作。在進一步地深入 WorkManager 之前,讓我們先來了解有哪些不同的背景工作型態。在下圖中,我們可以將背景工作分為三種型態(詳情請參照官網):
- Immediate:需要馬上被處理的工作,而且很快就會結束。
- Long-Running:需要一些時間才能處理完成的工作,有可能大於 10 分鐘。
- Deferrable:不需要立即處理的工作。可以安排在某個時間點來處理的工作。或是需要定期處理的工作。
在這三種型態中,每一種還可以細分為(詳情請參照官網):
- Persistent work:即使 app 重啟或是 device 重啟,仍然需要安排處理的工作。例如,我們希望可以確保將重要的資料完整地寫回資料庫,即使 process 強制結束也不會被中斷。
- Impersistent work:在 process 結束後,不需要安排處理的工作。
最終總共有六種背景工作型態(詳情請參照官網):
Category | Persistent | Impersistent |
---|---|---|
Immediate | WorkManager | Coroutines |
Long running | WorkManager | 不推薦 |
Deferrable | WorkManager | 不推薦 |
所有的 persistent 工作應該使用 WorkManager 來實作。而 immedidate 的 impersistent 工作,則應該使用 coroutines 來實作。
其中 long running 和 deferrable 的 impersistent 工作被標註為不推薦使用。因為 impersistent 工作不應該是 long running 或 deferrable 工作。Long running 工作所需時間較長,所以有可能因為 process 被終止而中斷。Deferrable 工作會在之後某個時間點被執行,所以有可能在被執行之前,app 就被重啟了。所以 long running 和 deferrable 工作應該只被考慮為 persisent 工作。
WorkManager
WorkManager 將 WorkRequest 儲存到資料庫出來實作 persistent 工作。所以即使 device 重啟後,WorkManager 可以從資料庫中取得 WorkRequest 資料,並重新排程。
除了 WorkManager 之外,Android 還提供其他方式來處理背景工作。在 Modern background execution in Android 這篇文章中,分析了幾種需要使用背景工作的情況,以及建議的解決方式。
Dependency
首先,我們引入 WorkManager 的 dependency。WorkManager 支援三種實作工作的方法。請根據你選定的方法,將相對應的 dependency 引入到你的 build.gradle。
第一種是純 Java 的工作。
dependencies { implementation "androidx.work:work-runtime:2.7.0" }
第二種是支援 Kotlin suspend 的工作。
dependencies { implementation "androidx.work:work-runtime-ktx:2.7.0" }
第三種是支援 RxJava2 的工作。
dependencies { implementation "androidx.work:work-runtime-rxjava2:2.7.0" }
在本文章,我們只會介紹第二種,也就是支援 Kotlin suspend 的工作。
建立工作
要建立一個工作,我們只需要繼承 CoroutineWorker
,並且實作 doWork()
即可。doWork()
會在背景中被 WorkManager 執行,並且最後要回傳一個 Result
。
- 如果工作執行成功,就回傳
Result.success()
。 - 如果工作執行失敗,就回傳
Result.failure()
。 - 如果工作執行失敗,想要讓 WorkManager 過一段時間再重試一次工作的話,就回傳
Result.retry()
。
在下方的程式碼中,我們建立一個 LogWorker
。在 doWork()
中,它從 inputData
中取得輸入的資料。這樣我們就可以在每次建立工作時,傳入不同的資料。
package com.waynestalk.workmanagerexample import android.content.Context import android.util.Log import androidx.work.CoroutineWorker import androidx.work.WorkerParameters class LogWorker(appContext: Context, workerParameters: WorkerParameters) : CoroutineWorker(appContext, workerParameters) { companion object { const val MESSAGE = "message" } private val tag = javaClass.canonicalName override suspend fun doWork(): Result { val message = inputData.getString(MESSAGE) ?: return Result.failure() Log.d(tag, message) return Result.success() } }
排程一次性工作
OneTimeWorkRequest可以用來建立只會被執行一次的工作。我們可以透過 OneTimeWorkRequestBuilder 來建立它,再呼叫 WorkManager.enqueue()
來排程工作。
在建立請求時,我們可以用 .setInputData()
傳入資料給工作,而 doWork()
可以用 inputData
來取得這些資料。另外,.setInitialDelay()
可以設定工作在多久後才會開始被執行。
val workRequest = OneTimeWorkRequestBuilder<LogWorker>() .setInputData(workDataOf(LogWorker.MESSAGE to "This is an one time work")) .setInitialDelay(1, TimeUnit.SECONDS) .build() WorkManager.getInstance(context).enqueue(workRequest)
排程週期性工作
PeriodicWorkRequest 可以建立一個週期性被執行的WorkRequest。我們可以透過 PeriodicWorkRequestBuilder 來建立它,並且要指定 interval 時間和 flex 時間。
- Interval:是指每個週期的時間長度。不可小於 PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS。
- Flex:是指工作會在這個時間內的任何時間點被啟動,並執行完畢。不可小於 PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS。
val workRequest = PeriodicWorkRequestBuilder<LogWorker>( PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS, ) .setInputData(workDataOf(LogWorker.MESSAGE to "This is a periodic work")) .setInitialDelay(1, TimeUnit.SECONDS) .build() WorkManager.getInstance(context).enqueue(workRequest)
工作的 Constraints
當工作有設定 Constraints 時,只有任務符合所有條件時,才會被執行。可以設定的條件如下:
- NetworkType:當符合需求的網路狀態,工作才會被執行。
- CONNECTED
- METERED
- NOT_REQUIRED
- NOT_ROAMING
- TEMPORARILY_UNMETERED
- UNMETERED
- BatteryNotLow:設定為 true 時,當裝置不在 low battery 模式時,工作才會被執行。
- RequiresCharging:設定為 true 時,當裝置在充電時,工作才會被執行。
- DeviceIdle:設定為 true 時,當裝置在閒置時,工作才會被執行。
- StorageNotLow:設定為 true 時,當裝置的儲存空間不低時,工作才會被執行。
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val workRequest = OneTimeWorkRequestBuilder<LogWorker>() .setInputData(workDataOf(LogWorker.MESSAGE to "This is an one time work with network constraint")) .setConstraints(constraints) .build() WorkManager.getInstance(context).enqueue(workRequest)
工作重試
在 doWork()
中,有可能因為某些原因無法執行工作。但是,我們不想直接回傳 Result.failure()
,反而希望 WorkManager 可以在重試工作。這時我們可以回傳 Result.retry()
。在 doWork()
中,還可以透過 runAttemptCount
取得已經重試的次數。
package com.waynestalk.workmanagerexample import android.content.Context import android.util.Log import androidx.work.CoroutineWorker import androidx.work.WorkerParameters class RetryLogWork(appContext: Context, workerParameters: WorkerParameters) : CoroutineWorker(appContext, workerParameters) { companion object { const val MESSAGE = "message" } private val tag = javaClass.canonicalName override suspend fun doWork(): Result { val message = inputData.getString(MESSAGE) ?: return Result.failure() Log.d(tag, "$message, retry count: $runAttemptCount") return Result.retry() } }
並且在建立 WorkRequest 時,還要設定 backoff delay 和 backoff policy。
- Backoff delay:第一次重試前,需要的等待時間。不可小於 OneTimeWorkRequest.MIN_BACKOFF_MILLIS。
- Backoff policy:每次重試時,backoff delay 增加的方式。
- LINEAR:Backoff delay * runAttemptCount
- EXPONENTIAL:Backoff delay * (2 ^ (runAttemptCount – 1))
val workRequest = OneTimeWorkRequestBuilder<RetryLogWork>() .setInputData(workDataOf(RetryLogWork.MESSAGE to "This is a retry work")) .setBackoffCriteria( BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS, ) .build() WorkManager.getInstance(context).enqueue(workRequest);
排程唯一性的工作
有時候我們會多次排程同一個工作,但是當已經有一個工作已經被排程時,我們可以讓新的工作取代現有的工作,或是直接忽略新的工作。呼叫 WorkManager.enqueueUniqueWork()
來排程唯一性的一次性工作,或是呼叫 WorkManager.enqueueUniquePeriodicWork()
來排程唯一性的週期性工作。
ExistingWorkPolicy 為當已經有一個現有的工作在排程時,應該要採取的行為。
- REPLACE:以新工作取代現有工作。這個選項會取消現有工作。
- KEEP:保留現有工作並忽略新工作。
- APPEND:將新工作串連到現有工作的後面,並在現有工作結束後執行。如果現有工作變成 CANCELLED 或 FAILED,則新工作也會變為 CANCELLED 或 FAILED。
- APPEND_OR_REPLACE:與 APPEND 類似,差別在於,如果現有工作是 CANCELLED 或 FAILED,新工作仍會執行。
val workRequest = OneTimeWorkRequestBuilder<LogWorker>() .setInputData(workDataOf(LogWorker.MESSAGE to "This is an unique one time work")) .setInitialDelay(5, TimeUnit.SECONDS) .build() WorkManager.getInstance(context).enqueueUniqueWork( "unique_one_time", ExistingWorkPolicy.REPLACE, workRequest, )
排程一連串的工作
最後,WorkManager 還可以讓我們排程一連串的工作。首先要呼叫 WorkManager.beginWith()
,之後都是呼叫 WorkManager.then()
。這兩個方法都可以接收一個 WorkRequest 或是 List<WorkRequest>。當傳入的是 List<WorkRequest> 時,list 中的工作會被平行地執行,而且當 list 中的工作都執行完後,才會開始下一個 WorkRequest.then() 的工作。
val workRequest1 = OneTimeWorkRequestBuilder<LogWorker>() .setInputData(workDataOf(LogWorker.MESSAGE to "Work 1")) .build() val workRequest2 = OneTimeWorkRequestBuilder<LogWorker>() .setInputData(workDataOf(LogWorker.MESSAGE to "Work 2")) .build() val workRequest3 = OneTimeWorkRequestBuilder<LogWorker>() .setInputData(workDataOf(LogWorker.MESSAGE to "Work 3")) .build() WorkManager.getInstance(context) .beginWith(listOf(workRequest1, workRequest2)) .then(workRequest3) .enqueue() }
結語
WorkManager 保證我們想要執行的程式碼會被執行,即使 device 重啟、app 重啟、或是 app 被終止。所以對於當 app 要儲存重要的資料時,WorkManager 提供了相當好的解決方案。