Android:用 WorkManager 排程工作

Photo by Janosch Diggelmann on Unsplash
Photo by Janosch Diggelmann on Unsplash
WorkManager 是官方推薦用來在背景處理 persistent 工作的 API。所謂的 persistent 工作指的是,即使 app 重啟或是 device 重啟,仍然需要安排處理的工作。本文章將介紹如何利用 WorkManager 來排程工作。

WorkManager 是官方推薦用來在背景處理 persistent 工作的 API。所謂的 persistent 工作指的是,即使 app 重啟或是 device 重啟,仍然需要安排處理的工作。本文章將介紹如何利用 WorkManager 來排程工作。

本章完整的程式碼可以在 下載。

Background Works

Android 提供數種方式讓開發者處理背景工作。在進一步地深入 WorkManager 之前,讓我們先來了解有哪些不同的背景工作型態。在下圖中,我們可以將背景工作分為三種型態(詳情請參照官網):

  • Immediate:需要馬上被處理的工作,而且很快就會結束。
  • Long-Running:需要一些時間才能處理完成的工作,有可能大於 10 分鐘。
  • Deferrable:不需要立即處理的工作。可以安排在某個時間點來處理的工作。或是需要定期處理的工作。

在這三種型態中,每一種還可以細分為(詳情請參照官網):

  • Persistent work:即使 app 重啟或是 device 重啟,仍然需要安排處理的工作。例如,我們希望可以確保將重要的資料完整地寫回資料庫,即使 process 強制結束也不會被中斷。
  • Impersistent work:在 process 結束後,不需要安排處理的工作。
Types of background work
Types of background work from Android Developers

最終總共有六種背景工作型態(詳情請參照官網):

CategoryPersistentImpersistent
ImmediateWorkManagerCoroutines
Long runningWorkManager不推薦
DeferrableWorkManager不推薦

所有的 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 這篇文章中,分析了幾種需要使用背景工作的情況,以及建議的解決方式。

Use Cases and Solutions
Use Cases and Solutions from 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。
Flexible run intervals
Flexible run intervals
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 提供了相當好的解決方案。

參考

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Hans-Jurgen Mager on Unsplash
Read More

Kotlin Coroutine 教學

Kotlin 的 coroutine 是用來取代 thread。它不會阻塞 thread,而且還可以被取消。Coroutine core 會幫你管理 thread 的數量,讓你不需要自行管理,這也可以避免不小心建立過多的 thread。
Read More