Làm việc ở chế độ nền bằng WorkManager

1. Trước khi bắt đầu

Lớp học lập trình này xoay quanh WorkManager, một thư viện đơn giản, linh hoạt và có khả năng tương thích ngược dành cho công việc có thể trì hoãn ở chế độ nền. WorkManager là trình lập lịch biểu được đề xuất cho tác vụ trên Android đối với công việc có thể trì hoãn nhưng đảm bảo được thực thi.

Điều kiện tiên quyết

Kiến thức bạn sẽ học được

Bạn sẽ thực hiện

  • Sửa đổi ứng dụng khởi đầu để dùng WorkManager.
  • Triển khai yêu cầu công việc để làm mờ hình ảnh.
  • Triển khai nhóm công việc nối tiếp bằng cách tạo chuỗi công việc.
  • Truyền dữ liệu vào và ra khỏi công việc đã được lên lịch.

Bạn cần có

  • Phiên bản ổn định mới nhất của Android Studio
  • Kết nối Internet

2. Tổng quan về ứng dụng

Ngày nay, khả năng chụp ảnh của điện thoại thông minh gần như đã quá tốt. Còn đâu cái thời mà nhiếp ảnh gia có thể chụp ra một bức ảnh mờ đáng tin về điều gì đó bí ẩn.

Trong lớp học lập trình này, bạn sẽ làm việc trên Blur-O-Matic, một ứng dụng làm mờ ảnh rồi lưu kết quả vào tệp. Liệu đó là quái vật hồ Loch Ness hay tàu ngầm đồ chơi? Nhờ Blur-O-Matic, sẽ chẳng ai biết được đâu!

Màn hình có các nút chọn để bạn chọn độ mờ của hình ảnh. Thao tác nhấp vào nút Start (Bắt đầu) sẽ làm mờ rồi lưu hình ảnh.

Hiện tại, Blur-O-Matic chưa áp dụng tính năng làm mờ hay lưu hình ảnh cuối cùng.

Lớp học lập trình này tập trung vào việc thêm WorkManager vào Blur-O-Matic, tạo ra worker giúp dọn dẹp tệp tạm thời được tạo bằng cách làm mờ hình ảnh rồi lưu bản sao cuối cùng của hình ảnh mà bạn xem được khi nhấp vào nút See File (Xem tệp). Bạn cũng tìm hiểu cách theo dõi trạng thái hoạt động ở chế độ nền và cập nhật giao diện người dùng của ứng dụng cho phù hợp.

3. Khám phá ứng dụng khởi đầu Blur-O-Matic

Lấy mã khởi đầu

Để bắt đầu, hãy tải mã khởi đầu xuống:

Ngoài ra, bạn có thể sao chép kho lưu trữ GitHub cho đoạn mã:

$ 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

Bạn có thể duyệt xem mã của ứng dụng Blur-O-Matic trong kho lưu trữ GitHub này.

Chạy mã khởi đầu

Để làm quen với mã khởi đầu, hãy hoàn thành các bước sau:

  1. Mở dự án bằng mã khởi đầu trong Android Studio.
  2. Chạy ứng dụng trên thiết bị Android hoặc một trình mô phỏng.

81ba9962a8649e70.png

Màn hình có các nút chọn để bạn chọn độ mờ của hình ảnh. Khi bạn nhấp vào nút Start (Bắt đầu), ứng dụng sẽ làm mờ rồi lưu hình ảnh.

Hiện tại, ứng dụng này chưa làm mờ khi bạn nhấp vào nút Start (Bắt đầu).

Hướng dẫn từng bước về mã khởi đầu

Trong nhiệm vụ này, bạn sẽ làm quen với cấu trúc của dự án. Dưới đây là các danh sách đưa ra hướng dẫn từng bước về các tệp và thư mục quan trọng trong dự án.

  • WorkerUtils: Các phương thức thuận tiện mà sau này bạn sẽ sử dụng để cho thấy Notifications và mã để lưu bitmap vào tệp.
  • BlurViewModel: Mô hình chế độ xem này lưu trữ trạng thái của ứng dụng và tương tác với kho lưu trữ.
  • WorkManagerBluromaticRepository: Lớp mà bạn bắt đầu công việc ở chế độ nền bằng WorkManager.
  • Constants: Một lớp tĩnh có một số hằng số bạn dùng trong lớp học lập trình này.
  • BluromaticScreen: Chứa các hàm có khả năng kết hợp cho giao diện người dùng và tương tác với BlurViewModel. Các hàm có khả năng kết hợp cho thấy hình ảnh và bao gồm các nút chọn để chọn độ mờ mong muốn.

4. WorkManager là gì?

WorkManager là một phần của Android Jetpack kiêm một Thành phần kiến trúc dành cho công việc ở chế độ nền cần kết hợp khả năng thực thi theo cơ hội và khả năng thực thi được đảm bảo. Thực thi theo cơ hội tức là WorkManager thực hiện công việc ở chế độ nền ngay khi có thể. Thực thi được đảm bảo tức là WorkManager xử lý logic để bắt đầu công việc của bạn trong nhiều tình huống, ngay cả khi bạn rời khỏi ứng dụng.

WorkManager là một thư viện cực kỳ linh hoạt kèm theo nhiều lợi ích khác. Có thể kể đến một số lợi ích như sau:

  • Hỗ trợ cả tác vụ định kỳ một lần và tác vụ một lần không đồng bộ.
  • Hỗ trợ quy tắc ràng buộc, chẳng hạn như điều kiện mạng, không gian lưu trữ và trạng thái sạc.
  • Tạo chuỗi yêu cầu công việc phức tạp, chẳng hạn như chạy công việc song song.
  • Tạo dữ liệu đầu ra cho một yêu cầu công việc được dùng làm dữ liệu đầu vào cho yêu cầu tiếp theo.
  • Xử lý khả năng tương thích cấp độ API dựa trên API cấp 14 (xem ghi chú).
  • Làm việc với hoặc không có Dịch vụ Google Play.
  • Làm theo các phương pháp hay nhất về tình trạng hệ thống.
  • Hỗ trợ để dễ dàng hiển thị trạng thái của yêu cầu công việc trong giao diện người dùng của ứng dụng.

5. Trường hợp sử dụng WorkManager

Thư viện WorkManager là lựa chọn hiệu quả cho nhiều tác vụ bạn cần hoàn thành. Quá trình chạy các tác vụ này không phụ thuộc vào việc ứng dụng có tiếp tục chạy sau khi tác vụ được đưa vào hàng đợi hay không. Các tác vụ sẽ chạy ngay cả khi ứng dụng đã đóng hoặc người dùng quay lại màn hình chính.

Dưới đây là một số ví dụ về tác vụ sử dụng hiệu quả WorkManager:

  • Định kỳ truy vấn các tin tức mới nhất.
  • Áp dụng bộ lọc cho hình ảnh rồi lưu hình ảnh.
  • Định kỳ đồng bộ hoá dữ liệu cục bộ với mạng.

WorkManager là một lựa chọn để chạy tác vụ ngoài luồng chính nhưng không phải là giải pháp chung để chạy mọi loại tác vụ ngoài luồng chính. Coroutine là một lựa chọn khác mà các lớp học lập trình trước đó đã thảo luận.

Để biết thêm thông tin chi tiết về thời điểm sử dụng WorkManager, hãy xem Hướng dẫn về công việc ở chế độ nền.

6. Thêm WorkManager vào ứng dụng

WorkManager yêu cầu phần phụ thuộc gradle theo sau. Phần này đã có trong tệp bản dựng:

app/build.gradle.kts

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

Bạn phải sử dụng phiên bản phát hành ổn định mới nhất của work-runtime-ktx trong ứng dụng.

Nếu bạn thay đổi phiên bản, đừng quên nhấp vào Sync Now (Đồng bộ hoá ngay) để đồng bộ hoá dự án với các tệp gradle đã cập nhật.

7. Thông tin cơ bản về WorkManager

Có một số lớp WorkManager mà bạn cần biết:

  • Worker / CoroutineWorker: Worker là lớp thực hiện đồng bộ công việc trên luồng nền. Khi quan tâm đến công việc không đồng bộ, chúng ta có thể sử dụng CoroutineWorker, công cụ có khả năng tương tác với coroutine của Kotlin. Trong ứng dụng này, bạn mở rộng từ lớp CoroutineWorker rồi ghi đè phương thức doWork(). Phương thức này là nơi bạn đặt mã cho công việc thực tế mà bạn muốn thực hiện ở chế độ nền.
  • WorkRequest: Lớp này thể hiện yêu cầu thực hiện một số công việc. WorkRequest là nơi bạn xác định xem worker cần được chạy một lần hay định kỳ. Bạn cũng có thể đặt quy tắc giới hạn trên WorkRequest để yêu cầu một số điều kiện cụ thể trước khi chạy công việc. Một ví dụ là thiết bị đang sạc trước khi bắt đầu công việc được yêu cầu. Bạn truyền CoroutineWorker vào quá trình tạo WorkRequest.
  • WorkManager: Lớp này thực sự lên lịch cho WorkRequest và buộc phương thức này phải chạy. Lớp này lên lịch cho WorkRequest theo cách phân bổ tải cho các tài nguyên hệ thống trong khi vẫn tuân thủ các quy tắc ràng buộc mà bạn chỉ định.

Trong trường hợp của bạn, bạn xác định một lớp BlurWorker mới, trong đó chứa mã để làm mờ hình ảnh. Khi bạn nhấp vào nút Start (Bắt đầu), WorkManager sẽ tạo rồi xếp một đối tượng WorkRequest vào hàng đợi.

8. Tạo BlurWorker

Ở bước này, bạn chụp ảnh trong thư mục res/drawable có tên là android_cupcake.png rồi chạy một vài hàm cho ảnh đó ở chế độ nền. Các hàm này làm mờ hình ảnh.

  1. Nhấp chuột phải vào gói com.example.bluromatic.workers trong ngăn dự án Android rồi chọn New -> Kotlin Class/File (Mới -> Tệp/Lớp Kotlin).
  2. Đặt tên cho lớp mới trong Kotlin là BlurWorker. Hãy mở rộng lớp này từ CoroutineWorker bằng các tham số hàm khởi tạo cần thiết.

workers/BlurWorker.kt

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

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

Lớp BlurWorker mở rộng lớp CoroutineWorker thay vì lớp Worker chung hơn. Quá trình triển khai lớp CoroutineWorker của doWork() là một hàm tạm ngưng cho phép chạy mã không đồng bộ, trong khi Worker không làm được việc này. Như đã nêu chi tiết trong hướng dẫn Tạo luồng trong WorkManager, "CoroutineWorker là phương thức triển khai được đề xuất cho người dùng Kotlin".

Tại thời điểm này, Android Studio sẽ vẽ một đường gợn sóng màu đỏ trong class BlurWorker cho biết có lỗi.

9e96aa94f82c6990.png

Nếu bạn đặt con trỏ lên văn bản class BlurWorker, IDE sẽ cho thấy một cửa sổ bật lên có thông tin bổ sung về lỗi đó.

ab230a408b2cbbe8.png

Thông báo lỗi cho biết bạn không ghi đè phương thức doWork() theo yêu cầu.

Trong phương thức doWork(), hãy viết mã để làm mờ hình ảnh bánh cupcake mà bạn thấy.

Hãy làm theo các bước sau để khắc phục lỗi và triển khai phương thức doWork():

  1. Đặt con trỏ vào trong mã lớp bằng cách nhấp vào văn bản "BlurWorker".
  2. Trên trình đơn của Android Studio, hãy chọn Code > Override Methods (Mã > Phương thức ghi đè)
  3. Trong cửa sổ Override Members (Ghi đè thành viên) bật lên, hãy chọn doWork()
  4. Nhấp vào OK.

685f803b01265e70.png

  1. Ngay trước khi khai báo lớp, hãy tạo một biến có tên TAG rồi chỉ định giá trị BlurWorker cho biến đó. Lưu ý rằng biến này không liên quan cụ thể đến phương thức doWork(), nhưng bạn sẽ sử dụng biến này cho các lệnh gọi đến Log().

workers/BlurWorker.kt

private const val TAG = "BlurWorker"

class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
...
  1. Để xem rõ hơn thời điểm thực thi công việc, bạn cần sử dụng hàm makeStatusNotification() của WorkerUtil. Hàm này giúp bạn dễ dàng hiển thị biểu ngữ thông báo ở đầu màn hình.

Bên trong phương thức doWork(), hãy dùng hàm makeStatusNotification() để hiển thị thông báo trạng thái và thông báo cho người dùng rằng worker làm mờ đã bắt đầu và đang làm mờ hình ảnh.

workers/BlurWorker.kt

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

    makeStatusNotification(
        applicationContext.resources.getString(R.string.blurring_image),
        applicationContext
    )
...
  1. Thêm một khối mã return try...catch, nơi thực hiện công việc làm mờ hình ảnh thực tế.

workers/BlurWorker.kt

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

        return try {
        } catch (throwable: Throwable) {
        }
...
  1. Trong khối try, hãy thêm lệnh gọi đến Result.success().
  2. Trong khối catch, hãy thêm lệnh gọi đến Result.failure().

workers/BlurWorker.kt

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

        return try {
            Result.success()
        } catch (throwable: Throwable) {
            Result.failure()
        }
...
  1. Trong khối try, hãy tạo một biến mới có tên là picture, điền biến đó bằng bitmap được trả về từ phương thức gọi BitmapFactory.decodeResource() rồi truyền vào gói tài nguyên của ứng dụng và mã tài nguyên của hình ảnh bánh cupcake.

workers/BlurWorker.kt

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

            Result.success()
...
  1. Làm mờ bitmap đó bằng cách gọi hàm blurBitmap() rồi truyền vào biến picture cùng giá trị 1 (một) cho tham số blurLevel.
  2. Lưu kết quả trong một biến mới có tên là output.

workers/BlurWorker.kt

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

            val output = blurBitmap(picture, 1)

            Result.success()
...
  1. Tạo một biến outputUri mới rồi điền vào biến này bằng lệnh gọi hàm writeBitmapToFile().
  2. Trong lệnh gọi đến writeBitmapToFile(), hãy truyền ngữ cảnh ứng dụng và biến output dưới dạng đối số.

workers/BlurWorker.kt

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

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

            Result.success()
...
  1. Thêm mã để cho người dùng thấy thông báo chứa biến outputUri.

workers/BlurWorker.kt

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

            makeStatusNotification(
                "Output is $outputUri",
                applicationContext
            )

            Result.success()
...
  1. Trong khối catch, hãy ghi lại một thông báo lỗi để cho biết đã xảy ra lỗi khi cố làm mờ hình ảnh. Lệnh gọi đến Log.e() truyền biến TAG được xác định trước đó, một thông báo thích hợp cũng như trường hợp ngoại lệ được gửi.

workers/BlurWorker.kt

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

Theo mặc định, CoroutineWorker, sẽ chạy dưới dạng Dispatchers.Default nhưng có thể thay đổi bằng cách gọi withContext() rồi truyền vào thành phần điều phối mong muốn.

  1. Tạo một khối withContext().
  2. Bên trong lệnh gọi đến withContext(), hãy truyền Dispatchers.IO để hàm lambda chạy trong một nhóm luồng đặc biệt nhằm chặn các hoạt động IO.
  3. Di chuyển mã return try...catch đã viết trước đó vào khối này.
...
        return withContext(Dispatchers.IO) {

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

Android Studio cho thấy lỗi sau đây vì bạn không gọi được return từ trong hàm lambda.

4de0966f4da38790.png

Bạn có thể sửa lỗi này bằng cách thêm nhãn như trong cửa sổ bật lên.

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

Vì Worker này chạy rất nhanh, bạn nên thêm độ trễ trong mã để mô phỏng công việc chạy chậm hơn.

  1. Bên trong hàm lambda withContext(), hãy thêm một lệnh gọi vào hàm số hiệu dụng delay() rồi truyền vào hằng số DELAY_TIME_MILLIS. Lệnh gọi này chỉ dành cho lớp học lập trình này nhằm cung cấp độ trễ cho nội dung thông báo.
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. Cập nhật WorkManagerBluromaticRepository

Kho lưu trữ xử lý mọi hành động tương tác với WorkManager. Cấu trúc này tuân theo nguyên tắc thiết kế phân tách các vấn đề và là mẫu kiến trúc Android được đề xuất.

  • Trong tệp data/WorkManagerBluromaticRepository.kt, bên trong lớp WorkManagerBluromaticRepository, hãy tạo một biến riêng tư có tên là workManager rồi lưu trữ một thực thể WorkManager trong đó bằng cách gọi WorkManager.getInstance(context).

data/WorkManagerBluromaticRepository.kt

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

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

Tạo và thêm WorkRequest vào hàng đợi trong WorkManager

Được rồi, giờ là lúc tạo WorkRequest và yêu cầu WorkManager chạy phương thức này! Có hai loại WorkRequest:

  • OneTimeWorkRequest: Một WorkRequest chỉ thực thi một lần.
  • PeriodicWorkRequest: Một WorkRequest thực thi nhiều lần trong một chu kỳ.

Bạn chỉ muốn làm mờ hình ảnh một lần khi nhấp vào nút Start (Bắt đầu).

Công việc này diễn ra trong phương thức applyBlur() mà bạn gọi khi nhấp vào nút Start (Bắt đầu).

Đã hoàn tất các bước sau đây bên trong phương thức applyBlur().

  1. Điền một biến mới có tên là blurBuilder bằng cách tạo một OneTimeWorkRequest cho worker làm mờ rồi gọi hàm mở rộng OneTimeWorkRequestBuilder qua WorkManager KTX.

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. Bắt đầu công việc bằng cách gọi phương thức enqueue() trên đối tượng workManager.

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. Chạy ứng dụng và xem thông báo khi bạn nhấp vào nút Start (Bắt đầu).

Hiện tại, hình ảnh được làm mờ như nhau, bất kể bạn chọn phương án nào. Ở các bước tiếp theo, thời gian làm mờ sẽ thay đổi tuỳ theo lựa chọn của bạn.

ff691da01e53addc.png

Để xác nhận hình ảnh đã được làm mờ thành công, bạn có thể mở Device Explorer (Trình khám phá thiết bị) trong Android Studio:

ba9937332cd1b985.png

Sau đó, hãy chuyển đến data > data > com.example.bluromatic > files > blur_filter_outputs > <URI> rồi xác nhận rằng hình ảnh bánh cupcake đã thật sự được làm mờ:

d2b7d8cfe8fc9d16.png

10. Dữ liệu đầu vào và dữ liệu đầu ra

Việc làm mờ thành phần hình ảnh trong thư mục tài nguyên đã rất tốt, nhưng để Blur-O-Matic thực sự là ứng dụng chỉnh sửa hình ảnh mang tính cách mạng, bạn phải tạo điều kiện để người dùng làm mờ hình ảnh họ thấy trên màn hình rồi đưa ra thành quả.

Để làm điều này, chúng ta cung cấp URI của hình ảnh bánh cupcake được hiển thị làm dữ liệu đầu vào cho WorkRequest, sau đó sử dụng kết quả của WorkRequest để cho thấy hình ảnh được làm mờ cuối cùng.

ce8ec44543479fe5.png

Dữ liệu đầu vào và đầu ra được chuyển vào và ra khỏi một worker thông qua đối tượng Data. Đối tượng Data là vùng chứa nhẹ cho cặp khoá/giá trị. Chúng lưu trữ một lượng nhỏ dữ liệu có thể truyền vào và ra khỏi worker từ WorkRequest.

Trong bước tiếp theo, bạn sẽ truyền URI đến BlurWorker bằng cách tạo đối tượng dữ liệu đầu vào.

Tạo đối tượng dữ liệu đầu vào

  1. Trong tệp data/WorkManagerBluromaticRepository.kt, bên trong lớp WorkManagerBluromaticRepository, hãy tạo một biến riêng tư mới có tên là imageUri.
  2. Điền sẵn URI hình ảnh vào biến bằng cách gọi phương thức ngữ cảnh getImageUri().

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

Mã ứng dụng chứa hàm trợ giúp createInputDataForWorkRequest() để tạo đối tượng dữ liệu đầu vào.

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

Trước tiên, hàm trợ giúp sẽ tạo một đối tượng Data.Builder. Tiếp theo, hàm trợ giúp thiết lập imageUriblurLevel vào đối tượng này dưới dạng cặp khoá/giá trị. Sau đó, một đối tượng Dữ liệu được tạo rồi trả về khi gọi return builder.build().

  1. Để thiết lập đối tượng dữ liệu đầu vào cho WorkRequest, hãy gọi phương thức blurBuilder.setInputData(). Bạn có thể tạo và truyền đối tượng dữ liệu trong một bước bằng cách gọi hàm trợ giúp createInputDataForWorkRequest() làm đối số. Đối với lệnh gọi đến hàm createInputDataForWorkRequest(), hãy truyền biến blurLevel và biến 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())
}

Truy cập đối tượng dữ liệu đầu vào

Bây giờ, hãy cập nhật phương thức doWork() trong lớp BlurWorker để lấy URI và mức độ mờ mà đối tượng dữ liệu đầu vào truyền vào. Nếu bạn không cung cấp giá trị cho blurLevel thì giá trị này sẽ được đặt mặc định là 1.

Bên trong phương thức doWork():

  1. Tạo một biến mới có tên là resourceUri rồi điền biến bằng cách gọi inputData.getString() và truyền vào hằng số KEY_IMAGE_URI được dùng làm khoá khi tạo đối tượng dữ liệu đầu vào.

val resourceUri = inputData.getString(KEY_IMAGE_URI)

  1. Tạo biến mới có tên là blurLevel. Điền biến bằng cách gọi inputData.getInt() và truyền vào hằng số BLUR_LEVEL dùng làm khoá khi tạo đối tượng dữ liệu đầu vào. Trong trường hợp bạn chưa tạo cặp khoá/giá trị này, hãy cung cấp giá trị mặc định là 1 (một).

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

Với URI, bây giờ, hãy làm mờ hình ảnh bánh cupcake trên màn hình.

  1. Kiểm tra để đảm bảo biến resourceUri được điền sẵn. Nếu mã này không được điền thì mã của bạn sẽ gửi một ngoại lệ. Mã theo sau sử dụng câu lệnh require() sẽ gửi IllegalArgumentException nếu đối số đầu tiên có giá trị false.

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
    }

Do nguồn hình ảnh được truyền vào dưới dạng URI, nên chúng ta cần đối tượng ContentResolver để đọc nội dung do URI trỏ đến.

  1. Thêm đối tượng contentResolver vào giá trị applicationContext.

workers/BlurWorker.kt

...
    require(!resourceUri.isNullOrBlank()) {
        // ...
    }
    val resolver = applicationContext.contentResolver
...
  1. Vì nguồn hình ảnh đã được truyền vào URI, hãy sử dụng BitmapFactory.decodeStream() thay vì BitmapFactory.decodeResource() để tạo đối tượng 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))
    )
  1. Truyền biến blurLevel trong lệnh gọi đến hàm blurBitmap().

workers/BlurWorker.kt

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

Tạo đối tượng dữ liệu đầu ra

Lúc này, bạn đã tìm hiểu xong Worker này và có thể trả về URI đầu ra dưới dạng đối tượng dữ liệu đầu ra trong Result.success(). Việc cung cấp URI đầu ra dưới dạng đối tượng dữ liệu đầu ra giúp các worker khác có thể dễ dàng truy cập các hoạt động khác. Phương pháp này hữu ích trong phần tiếp theo khi bạn tạo chuỗi woker.

Để làm việc này, vui lòng hoàn thành các bước sau:

  1. Trước mã Result.success(), hãy tạo một biến mới có tên là outputData.
  2. Điền biến này bằng cách gọi hàm workDataOf() rồi sử dụng hằng số KEY_IMAGE_URI cho khoá và biến outputUri làm giá trị. Hàm workDataOf() tạo đối tượng Dữ liệu qua cặp khoá và giá trị đã truyền.

workers/BlurWorker.kt

import androidx.work.workDataOf
// ...
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
  1. Cập nhật mã Result.success() để lấy đối tượng Dữ liệu mới này làm đối số.

workers/BlurWorker.kt

//Result.success()
Result.success(outputData)
  1. Xoá mã cho thấy thông báo vì không còn cần thiết nữa khi đối tượng Dữ liệu đầu ra đang sử dụng URI.

workers/BlurWorker.kt

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

Chạy ứng dụng

Tại thời điểm này, khi chạy ứng dụng, bạn có thể kỳ vọng ứng dụng sẽ tiến hành biên dịch. Bạn có thể thấy hình ảnh được làm mờ thông qua Trình khám phá thiết bị nhưng chưa thấy trên màn hình.

Lưu ý rằng có thể bạn phải Đồng bộ hoá để xem hình ảnh:

19b8f916bef89681.png

Tuyệt vời! Bạn đã làm mờ xong hình ảnh đầu vào bằng WorkManager!

11. Kết hợp Work thành chuỗi

Hiện tại, bạn đang làm một việc duy nhất là làm mờ hình ảnh. Tác vụ này là bước đầu tiên tuyệt vời, nhưng ứng dụng vẫn thiếu một số chức năng cốt lõi:

  • Ứng dụng không xoá các tệp tạm thời.
  • Ứng dụng không thực sự lưu hình ảnh vào một tệp vĩnh viễn.
  • Ứng dụng luôn làm mờ hình ảnh như nhau.

Bạn có thể sử dụng một chuỗi công việc trong WorkManager để thêm chức năng này. WorkManager hỗ trợ bạn tạo các WorkerRequest riêng biệt chạy theo thứ tự hoặc song song.

Trong phần này, bạn sẽ tạo một chuỗi công việc có dạng như sau:

c883bea5a5beac45.png

Các hộp này đại diện cho các WorkRequest.

Tính năng tạo chuỗi cũng có khả năng chấp nhận dữ liệu đầu vào và đầu ra. Dữ liệu đầu ra của một WorkRequest sẽ trở thành dữ liệu đầu vào của WorkRequest tiếp theo trong chuỗi.

Bạn đã có CoroutineWorker để làm mờ hình ảnh, nhưng cũng cần có CoroutineWorker để dọn dẹp các tệp tạm thời và CoroutineWorker để lưu hình ảnh vĩnh viễn.

Tạo CleanupWorker

CleanupWorker sẽ xoá các tệp tạm thời, nếu có.

  1. Nhấp chuột phải vào gói com.example.bluromatic.workers trong ngăn dự án Android rồi chọn New -> Kotlin Class/File (Mới -> Tệp/Lớp Kotlin).
  2. Đặt tên cho lớp mới trong Kotlin là CleanupWorker.
  3. Sao chép mã cho CleanupWorker.kt như trong mã ví dụ sau đây.

Vì thao tác đối với tệp nằm ngoài phạm vi của lớp học lập trình này, nên bạn có thể sao chép mã sau đây cho 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()
            }
        }
    }
}

Tạo SaveImageToFileWorker

Lớp SaveImageToFileWorker sẽ lưu tệp tạm thời vào một tệp vĩnh viễn.

SaveImageToFileWorker nhận dữ liệu đầu vào và đầu ra. Dữ liệu đầu vào là String của URI hình ảnh được làm mờ tạm thời, được lưu trữ bằng khoá KEY_IMAGE_URI. Dữ liệu đầu ra là String của URI hình ảnh được làm mờ đã lưu, được lưu trữ bằng khoá KEY_IMAGE_URI.

de0ee97cca135cf8.png

  1. Nhấp chuột phải vào gói com.example.bluromatic.workers trong ngăn dự án Android rồi chọn New -> Kotlin Class/File) (Mới -> Tệp/Lớp Kotlin).
  2. Đặt tên cho lớp mới trong Kotlin là SaveImageToFileWorker.
  3. Sao chép mã SaveImageToFileWorker.kt như trong mã ví dụ sau đây.

Vì thao tác đối với tệp nằm ngoài phạm vi của lớp học lập trình này, nên bạn có thể sao chép mã sau đây cho SaveImageToFileWorker. Trong mã được cung cấp, hãy lưu ý cách truy xuất và lưu trữ các giá trị resourceUrioutput bằng khoá KEY_IMAGE_URI. Quá trình này rất giống với mã bạn đã viết trước đây cho các đối tượng dữ liệu đầu vào và đầu ra.

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

Tạo chuỗi công việc

Hiện tại, mã chỉ tạo và chạy một WorkRequest duy nhất.

Ở bước này, bạn sửa đổi mã để tạo và thực thi một chuỗi WorkRequest thay vì chỉ một yêu cầu hình ảnh mờ.

Trong chuỗi WorkRequest, yêu cầu công việc đầu tiên của bạn là dọn dẹp các tệp tạm thời.

  1. Thay vì gọi OneTimeWorkRequestBuilder, hãy gọi workManager.beginWith().

Việc gọi phương thức beginWith() sẽ trả về một đối tượng WorkContinuation và tạo điểm bắt đầu cho một chuỗi WorkRequest có yêu cầu công việc đầu tiên trong chuỗi.

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

Bạn có thể thêm vào chuỗi yêu cầu công việc này bằng cách gọi phương thức then() và truyền một đối tượng WorkRequest.

  1. Xoá lệnh gọi đến workManager.enqueue(blurBuilder.build()) vì phương thức này chỉ đưa một yêu cầu công việc vào hàng đợi.
  2. Thêm yêu cầu công việc tiếp theo vào chuỗi bằng cách gọi phương thức .then().

data/WorkManagerBluromaticRepository.kt

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

// Add the blur work request to the chain
continuation = continuation.then(blurBuilder.build())
...
  1. Tạo một yêu cầu công việc để lưu hình ảnh rồi thêm hình ảnh đó vào chuỗi.

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. Để bắt đầu công việc, hãy gọi phương thức enqueue() trên đối tượng tiếp tục.

data/WorkManagerBluromaticRepository.kt

...
continuation = continuation.then(save)

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

Mã này tạo và chạy chuỗi WorkRequest sau đây: một CleanupWorker WorkRequest theo sau là một BlurWorker WorkRequest rồi đến một SaveImageToFileWorker WorkRequest.

  1. Chạy ứng dụng.

Giờ đây, bạn có thể nhấp vào Start (Bắt đầu) và xem thông báo khi các worker khác nhau thực thi. Bạn vẫn có thể thấy hình ảnh được làm mờ trong Trình khám phá thiết bị. Trong phần sắp tới, bạn sẽ thêm một nút bổ sung để người dùng có thể thấy hình ảnh được làm mờ trên thiết bị.

Trong các ảnh chụp màn hình sau đây, hãy chú ý rằng nội dung thông báo sẽ cho thấy worker nào hiện đang chạy.

9f82e140e72f0b72.png

ff93ddd7e2b44e28.png

b1fa3fc26bc09dd1.png

Lưu ý rằng thư mục đầu ra chứa nhiều hình ảnh được làm mờ: hình ảnh đang ở giai đoạn làm mờ trung gian và hình ảnh cuối cùng cho thấy hình ảnh kèm độ mờ bạn đã chọn.

Tuyệt vời! Giờ đây, bạn đã có thể dọn dẹp các tệp tạm thời, làm mờ hình ảnh và lưu hình ảnh!

12. Lấy đoạn mã giải pháp

Để tải mã này xuống khi lớp học lập trình đã kết thúc, bạn có thể sử dụng các lệnh sau:

$ 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

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp ZIP, sau đó giải nén rồi mở trong Android Studio.

Nếu bạn muốn xem mã giải pháp cho lớp học lập trình này, hãy xem mã đó trên GitHub.

13. Kết luận

Xin chúc mừng! Bạn đã tìm hiểu xong về ứng dụng Blur-O-Matic và sắp tìm hiểu về việc:

  • Thêm WorkManager vào dự án
  • Lên lịch cho OneTimeWorkRequest
  • Tham số đầu vào và đầu ra
  • Tạo chuỗi công việc WorkRequest

WorkManager hỗ trợ nhiều công việc hơn chúng ta có thể thảo luận trong lớp học lập trình này, bao gồm cả công việc lặp lại, thư viện hỗ trợ kiểm thử, yêu cầu công việc song song và công cụ hợp nhất dữ liệu đầu vào.

Để tìm hiểu thêm, hãy chuyển đến tài liệu về Lên lịch cho tác vụ bằng WorkManager.