使用 WorkManager 處理背景工作

1. 事前準備

本程式碼研究室將介紹 WorkManager,這是具有回溯相容性和彈性的簡易程式庫,適用於可延後執行的背景工作。WorkManager 是 Android 推薦的工作排程器,可用來處理可延後的工作,並確保工作順利執行。

必要條件

課程內容

執行步驟

  • 修改範例應用程式以使用 WorkManager。
  • 實作將圖片模糊處理的工作要求。
  • 鏈結工作,以便實作一系列的工作。
  • 將資料傳入和傳出已排定的工作。

軟硬體需求

  • 最新的 Android Studio 穩定版
  • 連上網際網路

2. 應用程式總覽

時至今日,智慧型手機的拍攝功能幾乎都「非常」強大。要針對某些神祕事物拍出模糊度可靠的相片,已不再是攝影師的專利。

在本程式碼研究室中,您將使用 Blur-O-Matic 應用程式對相片進行模糊處理,並將成品存檔。那是尼斯湖水怪還是玩具潛水艇?只要使用 Blur-O-Matic,沒有人能看得出來!

畫面中的圓形按鈕可讓您選擇圖片的模糊程度。按一下「Start」按鈕,即可將圖片模糊處理並儲存。

應用程式目前不會套用任何模糊效果,也不會儲存圖片的成品。

本程式碼研究室的重點包括在應用程式中加入 WorkManager、建立 worker 來清除為了模糊處理圖片而建立的暫存檔、對圖片進行模糊處理,以及儲存圖片的成品,讓您只要點選「See File」按鈕,就能查看成品。您也會瞭解如何監控背景工作的狀態,並據此更新應用程式的 UI。

3. 瞭解 Blur-O-Matic 範例應用程式

取得範例程式碼

如要開始使用,請先下載範例程式碼:

或者,您也可以複製 GitHub 存放區的程式碼:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git
$ cd basic-android-kotlin-compose-training-workmanager
$ git checkout starter

您可以在這個 GitHub 存放區中瀏覽 Blur-o-matic 應用程式的程式碼。

執行範例程式碼

若要瞭解範例程式碼,請完成下列步驟:

  1. 在 Android Studio 中開啟有範例程式碼的專案。
  2. 在 Android 裝置或模擬器中執行應用程式。

2bdb6fdc2567e96.png

畫面中有圓形按鈕可讓您選取圖片的模糊程度。只要按一下「Start」按鈕,應用程式就會將圖片模糊處理並儲存。

目前應用程式不會在您點選「Start」按鈕時套用任何模糊效果。

範例程式碼逐步操作說明

您可以透過這個工作熟悉專案結構。下列清單提供專案中重要檔案和資料夾的逐步操作說明。

  • WorkerUtils:之後要用來顯示 Notifications 和程式碼,以便將點陣圖儲存至檔案的便利方法。
  • BlurViewModel:這個檢視模型會儲存應用程式的狀態,並與存放區互動。
  • WorkManagerBluromaticRepository:透過 WorkManager 啟動背景工作的類別。
  • Constants:靜態類別,其中包含本程式碼研究室中使用的部分常數。
  • BluromaticScreen: 包含 UI 的可組合函式,並與 BlurViewModel 互動。可組合函式會顯示圖片,並且包含用來選取所需模糊處理程度的圓形按鈕。

4. 什麼是 WorkManager?

WorkManager 屬於 Android Jetpack 的一部分,也是一種架構元件,用於處理需結合「機會式執行」和「保證執行」的背景工作。機會式執行表示 WorkManager 會盡快執行背景工作。保證執行是指 WorkManager 會確保工作在不同情況下都能執行,即便您離開應用程式也一樣。

WorkManager 是非常靈活的程式庫,具有許多其他優勢。以下列出其中幾項優點:

  • 支援非同步的一次性工作和週期性工作。
  • 支援限制條件,如網路狀況、儲存空間和充電狀態等。
  • 複雜的工作要求鏈結,例如同時處理不同工作要求。
  • 一項工作要求產生的輸出內容,可做為下一個工作要求的輸入內容。
  • 將 API 級別相容性支援至 API 級別 14 以上 (請參見附註)。
  • 不論是否使用 Google Play 服務都能運作。
  • 遵循系統健康狀態最佳做法。
  • 支援在應用程式的 UI 中輕鬆顯示工作要求的狀態。

5. 使用 WorkManager 的時機

WorkManager 程式庫非常適合協助您完成各項必要工作。不論應用程式是否持續運作,只要把工作排入佇列就一定會執行。即使應用程式關閉,或使用者返回主畫面,工作照舊會執行。

以下列舉一些適合使用 WorkManager 的工作範例:

  • 定期查詢最新的新聞報導。
  • 為圖片套用篩選器,然後儲存圖片。
  • 定期將本機資料同步至網路。

WorkManager 是在主執行緒外執行工作的一種方式,但並非所有在主執行緒外執行的工作類型都適用。協同程式是另一種執行方式,先前的程式碼研究室曾討論過相關內容。

如要進一步瞭解 WorkManager 的使用時機,請參閱背景工作指南

6. 將 WorkManager 新增至應用程式

WorkManager 需要下列 Gradle 依附元件。建構檔案中已包含這項元件

app/build.gradle.kts

dependencies {
    // WorkManager dependency
    implementation("androidx.work:work-runtime-ktx:2.8.1")
}

您必須在應用程式中使用最新版 work-runtime-ktx穩定版

如果您變更了版本,請務必按一下「Sync Now」,將專案與更新過的 Gradle 檔案保持同步。

7. WorkManager 基本概念

您必須瞭解以下幾個 WorkManager 類別:

  • Worker/CoroutineWorker:worker 是一種在背景執行緒上同步執行工作的類別。由於我們希望進行非同步工作,因此可以使用 CoroutineWorker,它能與 Kotlin 協同程式互通。在此應用程式中,您會從 CoroutineWorker 類別擴充並覆寫 doWork() 方法。這個方法是用來放置您要在背景執行的實際工作程式碼。
  • WorkRequest:此類別代表執行某些工作的要求。WorkRequest 可讓您定義 worker 要執行一次還是定期執行。您也可以對 WorkRequest 設定限制條件,要求工作必須先滿足特定條件才能執行。例如,在開始執行要求的工作前,裝置要先充電。您會在建立 WorkRequest 的過程中傳入 CoroutineWorker
  • WorkManager:此類別會確實排定 WorkRequest 並將其執行。它會以在系統資源上分散負載的方式排定 WorkRequest,同時遵循您指定的限制條件。

在這個範例中,您可以定義新的 BlurWorker 類別,其中包含將圖片模糊處理的程式碼。當您點選「Start」按鈕時,WorkManager 會建立 WorkRequest 物件並排入佇列。

8. 建立 BlurWorker

在這個步驟中,您可以從 res/drawable 資料夾內取得名為 android_cupcake.png 的圖片,並在背景對該圖片執行幾項函式。這些函式會對圖片進行模糊處理。

  1. 在 Android 專案窗格中的 com.example.bluromatic.workers 套件上按一下滑鼠右鍵,然後依序選取「New」->「Kotlin Class/File」
  2. 將新的 Kotlin 類別命名為 BlurWorker。使用必要的建構函式參數從 CoroutineWorker 擴充該類別。

workers/BlurWorker.kt

import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import android.content.Context

class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
}

BlurWorker 類別會擴充 CoroutineWorker 類別,而非較通用的 Worker 類別。doWork()CoroutineWorker 類別實作是一項暫停函式,因此可執行 Worker 無法執行的非同步程式碼。如 WorkManager 中的執行緒指南中所述,「建議 Kotlin 使用者採用 CoroutineWorker 做為實作方式」。

此時,Android Studio 會在 class BlurWorker 下方繪製一條紅色的波浪線,表示發生錯誤。

9e96aa94f82c6990.png

如果將游標懸停在文字 class BlurWorker 上,IDE 會顯示彈出式視窗,其中含有與錯誤相關的額外資訊。

Cdc4bbefa7a9912b.png

錯誤訊息指出您並未按規定覆寫 doWork() 方法。

請在 doWork() 方法中編寫程式碼,將杯子蛋糕的圖片模糊處理。

請按照下列步驟修正錯誤並實作 doWork() 方法:

  1. 按一下「BlurWorker」文字,將游標移到類別代碼中。
  2. 在 Android Studio 選單中,依序選取「Code」>「Override Methods...」
  3. 在「Override Members」彈出式視窗中,選取 doWork()
  4. 按一下「OK」

8f495f0861ed19ff.png

  1. 在類別宣告之前,建立名為 TAG 的變數,並指派其值為 BlurWorker。請注意,這個變數與 doWork() 方法無關,但會在之後呼叫 Log() 時用到。

workers/BlurWorker.kt

private const val TAG = "BlurWorker"

class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
... 
  1. 如要更準確瞭解工作執行時間,您需要使用 WorkerUtilmakeStatusNotification() 函式。此函式可讓您輕鬆在畫面頂端顯示通知橫幅。

doWork() 方法中,使用 makeStatusNotification() 函式顯示狀態通知,並告知使用者已啟動模糊處理 worker,正在將圖片模糊處理。

workers/BlurWorker.kt

import com.example.bluromatic.R
...
override suspend fun doWork(): Result {

    makeStatusNotification(
        applicationContext.resources.getString(R.string.blurring_image),
        applicationContext
    )
...
  1. 新增 return try...catch 程式碼區塊,這是實際執行圖片模糊處理作業的位置。

workers/BlurWorker.kt

...
        makeStatusNotification(
            applicationContext.resources.getString(R.string.blurring_image),
            applicationContext
        )

        return try {
        } catch (throwable: Throwable) {
        }
...
  1. try 區塊中呼叫 Result.success()
  2. catch 區塊中呼叫 Result.failure()

workers/BlurWorker.kt

...
        makeStatusNotification(
            applicationContext.resources.getString(R.string.blurring_image),
            applicationContext
        )

        return try {
            Result.success()
        } catch (throwable: Throwable) {
            Result.failure()
        }
...
  1. try 區塊中,建立名為 picture 的新變數並填入點陣圖,該圖片是藉由呼叫 BitmapFactory.decodeResource() 方法,並傳入應用程式的資源套件以及杯子蛋糕圖片的資源 ID 所傳回。

workers/BlurWorker.kt

...
        return try {
            val picture = BitmapFactory.decodeResource(
                applicationContext.resources,
                R.drawable.android_cupcake
            )

            Result.success()
...
  1. 透過呼叫 blurBitmap() 函式並為 blurLevel 參數傳入 picture 變數和 1 的值 (一個),將點陣圖模糊處理。
  2. 將結果儲存在名為 output 的新變數中。

workers/BlurWorker.kt

...
            val picture = BitmapFactory.decodeResource(
                applicationContext.resources,
                R.drawable.android_cupcake
            )

            val output = blurBitmap(picture, 1)

            Result.success()
...
  1. 建立新的變數 outputUri,並呼叫 writeBitmapToFile() 函式來填入該變數。
  2. 在對 writeBitmapToFile() 的呼叫中,傳遞做為引數的應用程式結構定義 output 變數。

workers/BlurWorker.kt

...
            val output = blurBitmap(picture, 1)

            // Write bitmap to a temp file
            val outputUri = writeBitmapToFile(applicationContext, output)

            Result.success()
...
  1. 新增程式碼,向使用者顯示含有 outputUri 變數的通知訊息。

workers/BlurWorker.kt

...
            val outputUri = writeBitmapToFile(applicationContext, output)

            makeStatusNotification(
                "Output is $outputUri",
                applicationContext
            )

            Result.success()
...
  1. catch 區塊中,記錄錯誤訊息,指出嘗試模糊處理圖片時發生錯誤。呼叫 Log.e() 會傳遞先前定義的 TAG 變數、相對應的訊息,以及擲回的例外狀況。

workers/BlurWorker.kt

...
        } catch (throwable: Throwable) {
            Log.e(
                TAG,
                applicationContext.resources.getString(R.string.error_applying_blur),
                throwable
            )
            Result.failure()
        }
...

根據預設,CoroutineWorker, 會以 Dispatchers.Default 執行,但可藉由呼叫 withContext() 並傳入所需的調度工具來進行變更。

  1. 建立 withContext() 區塊。
  2. 在對 withContext() 的呼叫中傳遞 Dispatchers.IO,由於 lambda 函式可能會封鎖 IO 作業,因此會在特殊的執行緒集區中執行。
  3. 將先前編寫的 return try...catch 程式碼移入這個區塊。
...
        return withContext(Dispatchers.IO) {

            return try {
                // ...
            } catch (throwable: Throwable) {
                // ...
            }
        }
...

Android Studio 會顯示以下錯誤,因為您無法從 lambda 函式內呼叫 return

2d81a484b1edfd1d.png

只要新增標籤即可修正這項錯誤,如彈出式視窗中所示。

...
            //return try {
            return@withContext try {
...

由於此工作站執行速度非常快,因此建議您在程式碼中新增延遲,用來模擬執行速度較慢的工作。

  1. withContext() lambda 中,新增對 delay() 公用程式函式的呼叫並傳入 DELAY_TIME_MILLIS 常數。此呼叫為本程式碼研究室專用,用來在通知訊息之間提供延遲。
import com.example.bluromatic.DELAY_TIME_MILLIS
import kotlinx.coroutines.delay

...
        return withContext(Dispatchers.IO) {

            // This is an utility function added to emulate slower work.
            delay(DELAY_TIME_MILLIS)

                val picture = BitmapFactory.decodeResource(
...

9. 更新 WorkManagerBluromaticRepository

存放區會處理所有與 WorkManager 的互動。此結構遵循關注點分離的設計原則,也是我們建議的 Android 架構模式。

  • data/WorkManagerBluromaticRepository.kt 檔案的 WorkManagerBluromaticRepository 類別中,建立名為 workManager 的私有變數,並藉由呼叫 WorkManager.getInstance(context)WorkManager 例項儲存在其中。

data/WorkManagerBluromaticRepository.kt

import androidx.work.WorkManager
...
class WorkManagerBluromaticRepository(context: Context) : BluromaticRepository {

    // New code
    private val workManager = WorkManager.getInstance(context)
...

在 WorkManager 中建立 WorkRequest 並將其排入佇列

很好,現在我們來發送 WorkRequest,並要求 WorkManager 執行!WorkRequest 分為兩種類型:

  • OneTimeWorkRequest:只執行一次的 WorkRequest
  • PeriodicWorkRequest:在週期中重複執行的 WorkRequest

您想要在點選「Start」按鈕之後,只對圖片進行一次模糊處理。

此工作會在 applyBlur() 方法中執行,點選「Start」按鈕時就會呼叫該方法。

以下是在 applyBlur() 方法中完成的步驟。

  1. 透過為模糊處理 worker 建立 OneTimeWorkRequest,並從 WorkManager KTX 呼叫 OneTimeWorkRequestBuilder 擴充功能函式,填入名為 blurBuilder 的新變數。

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.BlurWorker
import androidx.work.OneTimeWorkRequestBuilder
...
override fun applyBlur(blurLevel: Int) {
    // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
}
  1. 透過對 workManager 物件呼叫 enqueue() 方法來啟動工作。

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.BlurWorker
import androidx.work.OneTimeWorkRequestBuilder
...
override fun applyBlur(blurLevel: Int) {
    // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()

    // Start the work
    workManager.enqueue(blurBuilder.build())
}
  1. 執行應用程式,然後在您點選「Start」按鈕時查看通知。

目前不論您選擇哪一個選項,圖片的模糊程度都相同。在後續步驟中,模糊程度會根據所選選項而有所差異。

f2b3591b86d1999d.png

如要確認圖片是否已成功模糊處理,請在 Android Studio 中開啟「Device Explorer」:

6bc555807e67f5ad.png

接著,依序前往「data」>「data」>「com.example.bluromatic」>「files」>「blur_filter_outputs」「<URI>」,確認杯子蛋糕的圖片已模糊處理:

fce43c920a61a2e3.png

10. 輸入資料和輸出資料

對資源目錄中的圖片素材進行模糊處理是不錯的做法,但為了讓 Blur-O-Matic 真正成為革命性的圖片編輯應用程式,您必須讓使用者能對畫面上顯示的圖片進行模糊處理,然後顯示經過模糊處理的圖片成品。

為此,我們要提供杯子蛋糕圖片的 URI 做為 WorkRequest 的輸入內容,然後使用 WorkRequest 的輸出內容來顯示最終的模糊圖片。

ce8ec44543479fe5.png

輸入和輸出內容會透過 Data 物件傳入及傳出 worker。Data 物件是鍵/值組合的輕量級容器,用來儲存可透過 WorkRequest 傳入或傳出 worker 的少量資料。

在下一個步驟中,您會建立輸入資料物件,將 URI 傳遞至 BlurWorker

建立輸入資料物件

  1. data/WorkManagerBluromaticRepository.kt 檔案的 WorkManagerBluromaticRepository 類別中,建立名為 imageUri 的新私有變數。
  2. 藉由呼叫結構定義方法 getImageUri(),以圖片 URI 填入變數。

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.getImageUri
...
class WorkManagerBluromaticRepository(context: Context) : BluromaticRepository {

    private var imageUri: Uri = context.getImageUri() // <- Add this
    private val workManager = WorkManager.getInstance(context)
...

應用程式程式碼包含用來建立輸入資料物件的 createInputDataForWorkRequest() 輔助函式。

data/WorkManagerBluromaticRepository.kt

// For reference - already exists in the app
private fun createInputDataForWorkRequest(blurLevel: Int, imageUri: Uri): Data {
    val builder = Data.Builder()
    builder.putString(KEY_IMAGE_URI, imageUri.toString()).putInt(BLUR_LEVEL, blurLevel)
    return builder.build()
}

首先,輔助函式會建立 Data.Builder 物件,然後將 imageUriblurLevel 做為鍵/值組合放入其中。接著,輔助函式會在呼叫 return builder.build() 物件時建立並傳回 Data 物件。

  1. 如要為 WorkRequest 設定輸入資料物件,請呼叫 blurBuilder.setInputData() 方法。只要呼叫 createInputDataForWorkRequest() 輔助函式做為引數,即可透過單一步驟建立和傳遞資料物件。如要呼叫 createInputDataForWorkRequest() 函式,請傳入 blurLevel 變數和 imageUri 變數。

data/WorkManagerBluromaticRepository.kt

override fun applyBlur(blurLevel: Int) {
     // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()

    // New code for input data object
    blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))

    workManager.enqueue(blurBuilder.build())
}

存取輸入資料物件

現在,讓我們更新 BlurWorker 類別中的 doWork() 方法,以取得輸入資料物件傳入的 URI 和模糊處理程度。如未提供 blurLevel 的值,則預設值為 1

doWork() 方法中:

  1. 建立名為 resourceUri 的新變數並填入變數,只要呼叫 inputData.getString(),並傳入建立輸入資料物件時用來做為鍵的常數 KEY_IMAGE_URI,即可填入變數。

val resourceUri = inputData.getString(KEY_IMAGE_URI)

  1. 建立名為 blurLevel 的新變數。如要填入變數,請呼叫 inputData.getInt(),並傳入建立資料物件時用來做為鍵的常數 BLUR_LEVEL。如果尚未建立這組鍵/值組合,請提供預設值 1 (一個)。

workers/BlurWorker.kt

import com.example.bluromatic.KEY_BLUR_LEVEL
import com.example.bluromatic.KEY_IMAGE_URI
...
override fun doWork(): Result {

    // ADD THESE LINES
    val resourceUri = inputData.getString(KEY_IMAGE_URI)
    val blurLevel = inputData.getInt(KEY_BLUR_LEVEL, 1)

    // ... rest of doWork()
}

現在,我們來使用 URI 對畫面上的杯子蛋糕圖片進行模糊處理。

  1. 檢查 resourceUri 變數是否已填入。如未填入,程式碼應該會擲回例外狀況。以下程式碼使用了 require() 陳述式,如果第一個引數評估結果為否 (false),就會擲回 IllegalArgumentException

workers/BlurWorker.kt

return@withContext try {
    // NEW code
    require(!resourceUri.isNullOrBlank()) {
        val errorMessage =
            applicationContext.resources.getString(R.string.invalid_input_uri)
            Log.e(TAG, errorMessage)
            errorMessage
    }

由於圖片來源是以 URI 的形式傳入,因此我們需要 ContentResolver 物件,用來讀取該 URI 指向的內容。

  1. contentResolver 物件新增至 applicationContext 值。

workers/BlurWorker.kt

...
    require(!resourceUri.isNullOrBlank()) {
        // ...
    }
    val resolver = applicationContext.contentResolver
...
  1. 由於目前圖片來源是透過 URI 傳遞,因此請使用 BitmapFactory.decodeStream() 建立點陣圖物件,不要使用 BitmapFactory.decodeResource()

workers/BlurWorker.kt

import android.net.Uri
...
//     val picture = BitmapFactory.decodeResource(
//         applicationContext.resources,
//         R.drawable.android_cupcake
//     )

    val resolver = applicationContext.contentResolver

    val picture = BitmapFactory.decodeStream(
        resolver.openInputStream(Uri.parse(resourceUri))
    )
  1. 將呼叫中的 blurLevel 變數傳遞至 blurBitmap() 函式。

workers/BlurWorker.kt

//val output = blurBitmap(picture, 1)
val output = blurBitmap(picture, blurLevel)

建立輸出資料物件

您已完成這個 worker,現在可以在 Result.success() 中將輸出 URI 做為輸出資料物件傳回。如果提供輸出 URI 做為輸出資料物件,其他 worker 就可以輕鬆地對該 worker 進行後續作業。在下一節建立 worker 鏈結時,這個方法會很有用。

如要這樣做,請按照下列步驟進行:

  1. Result.success() 程式碼之前,建立名為 outputData 的新變數。
  2. 如要填入這個變數,請呼叫 workDataOf() 函式,並使用常數 KEY_IMAGE_URI 做為鍵,變數 outputUri 做為值。workDataOf() 函式會根據傳入的鍵/值組合建立 Data 物件。

workers/BlurWorker.kt

import androidx.work.workDataOf
// ...
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
  1. 更新 Result.success() 程式碼,將這個新的 Data 物件做為引數。

workers/BlurWorker.kt

//Result.success()
Result.success(outputData)
  1. 移除顯示通知的程式碼,因為輸出 Data 物件現在使用的是 URI,因此不再需要該程式碼。

workers/BlurWorker.kt

// REMOVE the following notification code
//makeStatusNotification(
//    "Output is $outputUri",
//    applicationContext
//)

執行應用程式

此時,執行應用程式時,應用程式應該能進行編譯。您可以透過 Device Explorer 查看經過模糊處理的圖片,但還無法在螢幕上查看。

請注意,您可能需要同步處理才能看到圖片:

a658ad6e65f0ce5d.png

真厲害!您已成功使用 WorkManager 對輸入圖片進行模糊處理!

11. 鏈結工作

您目前正在進行的是將圖片模糊處理的工作。這項工作是最適合進行的第一步,但應用程式仍然缺少一些核心功能:

  • 應用程式不會清除暫存檔案。
  • 應用程式實際上並未將圖片儲存至永久檔案。
  • 應用程式會一律對圖片套用相同的模糊處理程度。

您可以使用 WorkManager 工作鏈結新增這項功能。WorkManager 可讓您建立個別 WorkerRequest,可以依序執行,也可以同時執行。

在本節中,您會建立一個工作鏈結,如下所示:

c883bea5a5beac45.png

方塊代表 WorkRequest

鏈結的另一個功能是可以接受輸入內容及產生輸出內容。一個 WorkRequest 的輸出內容會成為鏈結中下一個 WorkRequest 的輸入內容。

您已經使用 CoroutineWorker 對圖片進行模糊處理,但還需要 CoroutineWorker 來清除暫存檔案,並使用 CoroutineWorker 永久保存圖片。

建立 CleanupWorker

CleanupWorker 會刪除暫存檔案 (如果有的話)。

  1. 在 Android 專案窗格中的 com.example.bluromatic.workers 套件上按一下滑鼠右鍵,然後依序選取「New」->「Kotlin Class/File」
  2. 將新的 Kotlin 類別命名為 CleanupWorker
  3. 複製 CleanupWorker.kt 的程式碼,如以下程式碼範例所示。

由於檔案操控不屬於本程式碼研究室的課程範圍,您可以為 CleanupWorker 複製下列程式碼。

workers/CleanupWorker.kt

package com.example.bluromatic.workers

import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.example.bluromatic.DELAY_TIME_MILLIS
import com.example.bluromatic.OUTPUT_PATH
import com.example.bluromatic.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File

/**
 * Cleans up temporary files generated during blurring process
 */
private const val TAG = "CleanupWorker"

class CleanupWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {

    override suspend fun doWork(): Result {
        /** Makes a notification when the work starts and slows down the work so that it's easier
         * to see each WorkRequest start, even on emulated devices
         */
        makeStatusNotification(
            applicationContext.resources.getString(R.string.cleaning_up_files),
            applicationContext
        )

        return withContext(Dispatchers.IO) {
            delay(DELAY_TIME_MILLIS)

            return@withContext try {
                val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
                if (outputDirectory.exists()) {
                    val entries = outputDirectory.listFiles()
                    if (entries != null) {
                        for (entry in entries) {
                            val name = entry.name
                            if (name.isNotEmpty() && name.endsWith(".png")) {
                                val deleted = entry.delete()
                                Log.i(TAG, "Deleted $name - $deleted")
                            }
                        }
                    }
                }
                Result.success()
            } catch (exception: Exception) {
                Log.e(
                    TAG,
                    applicationContext.resources.getString(R.string.error_cleaning_file),
                    exception
                )
                Result.failure()
            }
        }
    }
}

建立 SaveImageToFileWorker

SaveImageToFileWorker 類別會將暫存檔案儲存至永久檔案。

SaveImageToFileWorker 會接受輸入和輸出內容。輸入內容是暫時經過模糊處理圖片的 URI 的 String,透過鍵 KEY_IMAGE_URI 進行儲存。輸出結果是已儲存的模糊圖片 URI 的 String,透過鍵 KEY_IMAGE_URI 進行儲存。

de0ee97cca135cf8.png

  1. 在 Android 專案窗格中的 com.example.bluromatic.workers 套件上按一下滑鼠右鍵,然後依序選取「New」->「Kotlin Class/File」
  2. 將新的 Kotlin 類別命名為 SaveImageToFileWorker
  3. 複製 SaveImageToFileWorker.kt 程式碼,如下列範例程式碼所示。

由於檔案操控不屬於本程式碼研究室的課程範圍,您可以為 SaveImageToFileWorker 複製下列程式碼。在提供的程式碼中,請注意如何透過鍵 KEY_IMAGE_URI 擷取及儲存 resourceUrioutput 值。這項程序與您先前為輸入和輸出資料物件編寫的程式碼非常類似。

workers/SaveImageToFileWorker.kt

package com.example.bluromatic.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.example.bluromatic.DELAY_TIME_MILLIS
import com.example.bluromatic.KEY_IMAGE_URI
import com.example.bluromatic.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Date

/**
 * Saves the image to a permanent file
 */
private const val TAG = "SaveImageToFileWorker"

class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {

    private val title = "Blurred Image"
    private val dateFormatter = SimpleDateFormat(
        "yyyy.MM.dd 'at' HH:mm:ss z",
        Locale.getDefault()
    )

    override suspend fun doWork(): Result {
        // Makes a notification when the work starts and slows down the work so that
        // it's easier to see each WorkRequest start, even on emulated devices
        makeStatusNotification(
            applicationContext.resources.getString(R.string.saving_image),
            applicationContext
        )

        return withContext(Dispatchers.IO) {
            delay(DELAY_TIME_MILLIS)

            val resolver = applicationContext.contentResolver
            return@withContext try {
                val resourceUri = inputData.getString(KEY_IMAGE_URI)
                val bitmap = BitmapFactory.decodeStream(
                    resolver.openInputStream(Uri.parse(resourceUri))
                )
                val imageUrl = MediaStore.Images.Media.insertImage(
                    resolver, bitmap, title, dateFormatter.format(Date())
                )
                if (!imageUrl.isNullOrEmpty()) {
                    val output = workDataOf(KEY_IMAGE_URI to imageUrl)

                    Result.success(output)
                } else {
                    Log.e(
                        TAG,
                        applicationContext.resources.getString(R.string.writing_to_mediaStore_failed)
                    )
                    Result.failure()
                }
            } catch (exception: Exception) {
                Log.e(
                    TAG,
                    applicationContext.resources.getString(R.string.error_saving_image),
                    exception
                )
                Result.failure()
            }
        }
    }
}

建立工作鏈結

目前,程式碼只會建立及執行單一 WorkRequest

在這個步驟中,您必須修改程式碼來建立並執行 WorkRequest 鏈結,而非只執行一個模糊處理圖片要求。

在 WorkRequests 鏈結中,您的第一個工作要求是清理暫存檔案。

  1. 請呼叫 workManager.beginWith(),不要呼叫 OneTimeWorkRequestBuilder

呼叫 beginWith() 方法會傳回 WorkContinuation 物件,並為含有第一個工作請求的 WorkRequest 鏈結建立起始點。

data/WorkManagerBluromaticRepository.kt

import androidx.work.OneTimeWorkRequest
import com.example.bluromatic.workers.CleanupWorker
// ...
    override fun applyBlur(blurLevel: Int) {
        // Add WorkRequest to Cleanup temporary images
        var continuation = workManager.beginWith(OneTimeWorkRequest.from(CleanupWorker::class.java))

        // Add WorkRequest to blur the image
        val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
...

只要呼叫 then() 方法並傳入 WorkRequest 物件,即可新增至此工作要求鏈結。

  1. 移除對 workManager.enqueue(blurBuilder.build()) 的呼叫,這個呼叫只會將一個工作要求加入佇列。
  2. 呼叫 .then() 方法,將下一個工作要求新增至鏈結中。

data/WorkManagerBluromaticRepository.kt

...
//workManager.enqueue(blurBuilder.build())

// Add the blur work request to the chain
continuation = continuation.then(blurBuilder.build())
...
  1. 建立儲存圖片的工作要求,並將要求新增至鏈結中。

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.SaveImageToFileWorker

...
continuation = continuation.then(blurBuilder.build())

// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
    .build()
continuation = continuation.then(save)
...
  1. 如要啟動工作,請在接續物件上呼叫 enqueue() 方法。

data/WorkManagerBluromaticRepository.kt

...
continuation = continuation.then(save)

// Start the work
continuation.enqueue()
...

這個程式碼會產生並執行以下的 WorkRequest 鏈結:CleanupWorker WorkRequest,後面接著 BlurWorker WorkRequestSaveImageToFileWorker WorkRequest

  1. 執行應用程式。

您現在可以按一下「Start」,並在不同 worker 執行時看到通知。您仍然能在 Device Explorer 看到經過模糊處理的圖片。在接下來的章節中,您將新增另一個按鈕,方便使用者在裝置上查看經過模糊處理的圖片。

請注意,在下列螢幕截圖中,通知訊息會顯示目前正在執行的 worker。

bbe0fdd79e3bca27.png

5d43bbfff1bfebe5.png

da2d31fa3609a7b1.png

請注意,輸出資料夾中會有多張經過模糊處理的圖片,包括還在進行模糊處理的圖片,以及依據您選擇的模糊程度呈現的圖片成品。

太棒了!現在您可以清除暫存檔案、將圖片模糊處理並且儲存圖片!

12. 取得解決方案程式碼

完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用這些指令:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git
$ cd basic-android-kotlin-compose-training-workmanager
$ git checkout intermediate

另外,您也可以下載存放區為 ZIP 檔案,然後解壓縮並在 Android Studio 中開啟。

如要查看本程式碼研究室的解決方案程式碼,請前往 GitHub

13. 結論

恭喜!您已完成 Blur-O-Matic 應用程式,並且在過程中瞭解如何進行以下操作:

  • 將 WorkManager 新增至專案
  • 排定 OneTimeWorkRequest
  • 輸入和輸出參數
  • 將工作的 WorkRequest 鏈結在一起

WorkManager 還支援更多本程式碼研究室未涵蓋的功能,包括重複性工作、測試支援資料庫、同時執行不同工作要求,以及合併輸入內容。

詳情請參閱使用 WorkManager 安排工作說明文件。