+3

Work Trong Background Với WorkManager (Part I)

Tổng quan

Trong bài viết này chúng ta sẽ tìm hiểu về WorkManager là gì và áp dụng WorkManager vào một sample project được viết bằng Jetpack compose.

Những gì bạn sẽ học

Những gì bạn sẽ làm

  • Sử dụng Workmanager trong project được viết bằng jetpack compose
  • Thực hiện một work request làm mờ (blur) image
  • Thực hiện một nhóm các task (Work) theo tuần tự bằng cách xâu chuỗi các task (Work)
  • Truyền data vào và ra giữa các task (Work)

Tổng quan về sample project Trong bài viết này chúng ta sẽ tạo một app làm mờ (blur) image giao diện như bên dưới.

Màn hình có các radio button được để lựa chọn các mức độ làm mờ (blur) image. Khi click vào button Start làm mờ (blur) và lưu lại image. Bài viết chỉ tập trung vào việc sử dụng WorkManager trong ứng dụng, tạo các work request để xoá file image tạm được tạo để làm mờ image, làm mờ (blur) một image, lưu image sau khi đã làm mờ. Cấu trúc của sample project gồm những file quan trọng được sử dụng:

  • WorkerUtils: có các method để hiển thị Notification và code dùng để lưu một ảnh bitmap vào file
  • BlurViewModel: được sử dụng để lưu state của app và tương tác với repository
  • WorkManagerBlurRepository: class nơi bắt đầu những task (Work) trong background với WorkManager
  • Constants: chứa những biến constant được sử dụng trong app
  • BlurScreen: chứa những compose functions cho việc tạo UI và tương tác với BlurViewModel. Màn hình chính hiển thị image và các radio buton để user có thể lựa chọn các level làm mờ image

WorkManager là gì?

WorkManager là một phần của Android JetpackArchitecture Component thực hiện những công việc dưới background. Những công việc dưới background sẽ đảm bảo chắc chắn được thực hiện ngay khi nhưng điều kiện rằng buộc phù hợp ngay cả khi user điều hướng khỏi ứng dụng. WorkManager là một thư viện vô cùng linh hoạt và có nhiều lợi ích bổ sung. Một vài những lợi ích như :

  • Hỗ trợ thực thi những work request không đồng bộ (asynchronous) một lần hoặc định kỳ
  • Xâu chuỗi các work thực thi theo tuần tự hoặc thực thi work song song.
  • Output của một work được sử dụng như input của work tiếp theo
  • Hỗ trợ API level 14
  • Làm việc cùng hoặc không với Google Play Service
  • Hỗ trợ việc hiển thị status của work trong UI của app WorkManager được khuyến khích sử dụng hơn những libaray có cùng tính năng như JobSchedulerAlarmManager

Khi nào nên sử dụng WorkManager?

Thư viện WorkManager là một sự lựa chọn tốt cho những công việc (work) mà bạn cần phải hoàn thành. Việc thực thi những công việc (work) không phụ thuộc vào việc ứng dụng có chạy hay không. Những công việc (work) được thực thi thậm chí khi ứng dựng đã bị close hoặc user trở về màn hình home. Một số ví dụ về việc sử dụng WorkManager:

  • Thực hiện việc truy vấn định kỳ để biết những tin tức mới nhất
  • Áp dụng cho việc filter image and sau đó lưu image
  • Định kỳ việc đồng bộ data ở local với network WorkManager là một sự lựa chọn cho việc thực thi các công việc (work) bên ngoài main thread nhưng nó không phải là giải pháp tổng thể để chạy mọi loại công việc ngoài main thread (Coroutines làm một sự lựa chọn khác). Để biết thêm chi tiết về khi nào nên sử dụng WorkManager có thể tham khảo thêm Guide to background work.

Add WorkManager vào Project

app/build.gradle.kts

Click Sync Now để đồng bộ (sync) lại project của bạn sau khi file gradle được update

Cơ bản về WorkManager

Trong WorkManager có vài class mà bạn cần biết.

  • Worker / CoroutineWorker: Worker là một class dùng để thực thi công việc đồng bộ dưới background. Vì chúng ta quan tâm tới việc thực hiện những công việc không đồng bộ nên chúng ta sẽ sử dụng CoroutineWorker, cái có khẳng tương tác với Kotlin Coroutine. Trong ứng dụng bạn sẽ extend từ class CoroutineWorker và override method doWork(). Phương thức này là nơi bạn sẽ viết code để thực hiện các công việc mà bạn muốn thực hiện dưới background.
  • WorkRequest: class này đại diện cho một request mà bạn muốn làm một số công việc. WorkRequest là nơi bạn định nghĩa các công việc bạn muốn chạy một lần hay định kỳ. Các điều kiện rằng buộc (Constraints) cũng được config trong WorkRequest để yêu cầu những điều kiện cụ thể phù hợp trước khi thực hiện một công việc. Một ví dụ đó là device đang được sạc pin trước khi bắt đầu thực hiện một work request.
  • WorkManager: class này dùng để lên lịch trình (schedule) thực hiện các WorkRequest và đảm bảo các công việc được thực thi. Nó lên lịch thực hiện một WorkRequest theo cách phân bổ tài nguyên của hệ thống đồng thời đảm bảo các rằng buộc thực hiện công việc mà bạn đã config trong WorkRequest.

Tạo BlurWorker

Tại thời điểm này bạn sẽ lấy một image trong res/drawable được gọi là android_cupcake.png và thực thi một số function trên nó trong background. Những function này sẽ làm mờ (blur) image. Bạn tạo 1 file tên là BlurWorker

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 là một class được extend từ CoroutineWorker. CoroutineWorker thực thi method doWork() là một suspend function, cho phép nó chạy code không đồng bộ. CoroutineWorker được recommend cho người dùng kotlin. Tại thời điểm này sẽ nhận được thông báo lỗi.

Bạn cần phải override method** doWork()** . Trong method doWork() sẽ gọi một method makeStatusNotification() để hiển thị notification thông báo tới user rằng BlurWork đã được start và đang làm mờ (blur) image.

workers/BlurWorker.kt

import com.tech.codelab.blurimage.R
...
override suspend fun doWork(): Result {

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

Add khối return try...catch nơi sẽ thực thi tác vụ làm mờ (blur) image

workers/BlurWorker.kt

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

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

Trong khối try gọi Result.success() Trong khối catch gọi Result.failure() WorkManager sử dụng Result.success() và Result.failure() để chỉ trạng thái cuối cùng của các công việc được request thực hiện.

workers/BlurWorker.kt

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

        return try {
            Result.success()
        } catch (throwable: Throwable) {
            Result.failure()
        }
...

Trong khối try bạn tạo một biến tên picture để chưa ảnh bitmap và gọi method blurBitmap(). Truyền vào method này một ảnh bitmap và giá trị là 1(cấp độ làm mờ ảnh)

workers/BlurWorker.kt

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

            val output = blurBitmap(picture, 1)

            Result.success()
...

Tạo một biến outputUri và goi method writeBitmapToFile()

workers/BlurWorker.kt

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

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

            Result.success()
...

gọi method makeStatusNotification() để thông báo tới user về trạng thái thực hiện tác vụ bằng việc đính kèm giá trị của outputUri

workers/BlurWorker.kt

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

            makeStatusNotification(
                "Output is $outputUri",
                applicationContext
            )

            Result.success()
...

CoroutineWorker mạc định chạy trong Dispatchers.Default nhưng có thể thay đổi bằng cách gọi withContext() và truyền vào dispatcher mong muốn. Move code bạn đã viết trong khối return try...catch vào trong như bên dưới.

...
        return withContext(Dispatchers.IO) {

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

Tại thời điểm này Android Studio sẽ hiển thị lỗi bởi vì bạn không thể gọi return từ bên trong một funtion lambda

Lỗi này được fix như bên dưới.

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

Cho mục địch demo nên trong withContext() lambda gọi method delay() như bên dưới

import com.tech.codelab.blurimage.DELAY_TIME_MILLIS
import kotlinx.coroutines.delay

...
            return@withContext try {

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

                val picture = BitmapFactory.decodeResource(
...

Update WorkManagerBlurRepository

Repository xử lý tất cả các tương tác với WorkManager. Trong data/WorkManagerBlurRepository.kt bên trong class** WorkManagerBlurRepository** tạo một biến private workManager và lưu trữ 1 thể hiện WorkManager trong nó bằng cách gọi WorkManager.getInstance(context)

data/WorkManagerBlurRepository.kt

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

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

** Tạo WorkRequest trong WorkManager** Tại thời điểm này chúng ta sẽ tạo một WorkRequest và nói với WorkManager thực thi nó. Có 2 loại WorkRequest:

  • OneTimeWorkRequest: một WorkRequest chỉ thực thi một lần
  • PeriodicWorkRequest: một WorkRequest thực thi lặp đi lặp lại trong một chu kỳ. Trong sample code bạn chỉ muốn image được làm mờ (blur) một lần khi button Start được click. Các đoạn code bên dưới được hoàn thành trong method applyBlur() Tạo một biến có tên blurBuilder bằng cách tạo một OneTimeWorkRequest cho BlurWorker.

data/WorkManagerBlurRepository.kt

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

Để bắt đầu một Woker bằng cách gọi method enqueue() của workManager

data/WorkManagerBlurRepository.kt

import com.tech.codelab.blurimage.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())
}

Chạy sample bạn sẽ nhìn thấy notification khi bạn click button Start

Để xác nhận image đã được làm mờ blur thành công. Bạn có thể mở Device File Explorer trong Android Studio.

Input data và output data

Trong phần này chúng ta sẽ gửi URI của image mà chúng ta đã hiển thị trong sample như là input của WorkRequest và sau đó sử dụng output của WorkRequest để hiển thị kết quả sau cùng của image sau khi làm mờ(blur).

Input và output là được gửi vào và ra của một Worker thông qua Object Data. Object Data là nhẹ và chứa cặp key/value. Được dùng lưu trữu các loại data nhỏ như String, Long, Int... Chúng ta sẽ gửi URI tơi BlurWorker bằng cách tạo một input object data. Trong file data/WorkManagerBlurRepository.kt , bên trong class WorkManagerBluromaticRepository tạo một biến tên là imageUri như bên dưới.

data/WorkManagerBlurRepository.kt

import com.tech.codelab.blurimage.getImageUri
...
class WorkManagerBlurRepository(context: Context) : BlurRepository {

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

Trong sample có tạo một method createInputDataForWorkRequest() cho việc tạo input object data

data/WorkManagerBlurRepository.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()
}

Để set input data cho WorkRequest, bạn có thể gọi method blurBuilder.setInputData()

data/WorkManagerBlurRepository.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())
}

Truy cập để nhận input data Bây giờ chúng ta cần phải update lại method doWork() trong class BlurWorker mà chúng ta đã tạo ở trên để lấy về URI và level blur mà chúng ta đã gửi vào trong thông qua input data

workers/BlurWorker.kt

import com.tech.codelab.blurimage.KEY_BLUR_LEVEL
import com.tech.codelab.blurimage.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()
}

Check gía trị của biến resourceUri nếu là null or bank thì sẽ throw một exception bằng cách sử dụng require() để throw ra 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
    }

Bởi vì image bây giờ được gửi vào thông qua URI nên sẽ sử dụng BitmapFactory.decodeStream() thay cho BitmapFactory.decodeResource() để tạo một Bitmap

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))
                 )

Gửi gía trị của biến blurLevel vào trong method blurBitmap() workers/BlurWorker.kt

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

Tạo output data Bạn có thể trả về một output là URI của image bạn vừa làm mờ (blur) như là một output data và gửi vào trong Result.success(). Sử dụng method workDataOf() cho việc tạo Data như bên dưới.

workers/BlurWorker.kt

import androidx.work.workDataOf
// ...
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

Update lại code của** Result.success()** với một đối số đầu vào là một object data.

//Result.success()
Result.success(outputData)

Chạy app và bạn sẽ nhìn thấy image được làm mờ thông qua Device File Explorer. Chú ý là có thể bạn sẽ cần phải Synchronize để nhìn thấy kết quả.

Chuỗi các WorkRequest

WorkManager cho phép bạn tạo các WorkRequest để chạy theo thứ tự hoặc song song. Trong phần này bạn sẽ tạo một chuỗi các WorkRequest cho việc thực hiện các công việc theo thứ tự: xoá file image tập thời, làm mở image, lưu image sau khi làm mở như bên dưới.

Tạo CleanupWorker

workers/CleanupWorker.kt

package com.tech.codelab.blurimage.workers

import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.tech.codelab.blurimage.DELAY_TIME_MILLIS
import com.tech.codelab.blurimage.OUTPUT_PATH
import com.tech.codelab.blurimage.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File
import kotlin.Exception

/**
 * Clean 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
         */
        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) {
                exception.printStackTrace()
                Result.failure()
            }
        }
    }

}

Tạo SaveImageToFileWorker

SaveImageToFileWorker lấy một input và output. Input là một URI của image đã được làm mờ và lưu với key KEY_IMAGE_URI.

workers/SaveImageToFileWorker.kt

package com.tech.codelab.blurimage.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.tech.codelab.blurimage.DELAY_TIME_MILLIS
import com.tech.codelab.blurimage.KEY_IMAGE_URI
import com.tech.codelab.blurimage.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.util.Locale
import java.text.SimpleDateFormat
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.
        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.isNullOrBlank()) {
                    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()
            }
        }
    }
}

**Tạo một chuỗi các WorkRequest **

Thay vì gọi OneTimeWorkRequestBuilder, chúng ta sẽ gọi workManager.beginWith() phương thức trả về một object WorkContinuation là điểm bắt đầu cho một chuỗi WorkRequest

data/WorkManagerBlurRepository.kt

import com.tech.codelab.blurimage.workers.CleanupWorker
import com.tech.codelab.blurimage.workers.SaveImageToFileWorker
// ...
    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>()
...

Chý ý trong đoạn code trên chúng ta đã sử dụng OneTimeWorkRequest để tạo một WorkRequest bằng cách gọi OneTimeWorkRequest.from(CleanupWorker::class.java). Cách này tương tự như OneTimeWorkRequestBuilder<CleanupWorker>().build(). OneTimeWorkRequest đến từ thư viện AndroidX trong khi OnTimeWorkRequestBuilder là một helper function được cung cấp bới WorkManager KTX extension. Remove workManager.enqueue(blurBuilder.build()). Thêm một WorkRequest vào chuỗi bằng cách gọi method .then(). Để bắt đầu chuỗi thực thi chuỗi WorkRequest ta gọi method enqueue() trên object continuation

        var continuation = workManager.beginWith(OneTimeWorkRequest.Companion.from(CleanupWorker::class.java))

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

        // For input data object
        blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))


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

        // Add WorkRequest to save the image to the filesystem
        val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>().build()

        // Start the work
//        workManager.enqueue(blurBuilder.build())
        continuation = continuation.then(save)
        continuation.enqueue()

Chạy ứng dụng và bạn click button Start

Các bạn có thể tham khảo Sample trên Github


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí