WorkManager Basics
Bài đăng này đã không được cập nhật trong 5 năm
WorkManager Basics
WorkManager là một Android Jetpack library, được xây dựng nhằm mục đích schedule deferrable, asynchronous tasks. Trong post này mình sẽ nói về WorkManager
là gì và khi nào sử dụng nó.
Bắt đầu với ví dụ
Giả sử chúng ta có một app chỉnh sửa hình ảnh, cho phép put các filters và upload nó lên web cho mọi người cùng xem. Bây giờ chúng ta cần tạo một series các background tasks bao gồm việc áp dụng các filter, nén image, và sau đó upload nó. Trong mỗi phase, có một vài hạn chế cần được kiểm tra đó là: thiết bị có đủ pin khi đang lọc image hay không, có đủ vùng lưu trữ khi nén image hay không, hay có kết nối internet khi đang upload hay không.
Ví dụ của một task:
- Deferrable, bởi vì chúng ta không cần nó xảy ra ngay lập tức, và trong thực tế có thể chờ một số ràng buộc được đáp ứng (như chờ một kết nối mạng).
- Cần được guaranteed để chạy, bất kể app có bị thoát ra hay không, bởi vì người dùng sẽ không hề thoải mái nếu image đã được lọc của họ không bao giờ được share cho mọi người thấy.
Những đặc điểm trên làm cho image filter và uploading tasks trở thành trường hợp hoàn hảo để sử dụng WorkManager.
Thêm WorkManager dependency
Toàn bộ Code trong post này sẽ được viết bằng Kotlin, sử dụng KTX library (Kotlin extensions). KTX cung cấp các extensions fuction giúp Kotlin ngắn gọn và rõ ràng hơn. Chúng ta dùng phiên bản KTX của WorkManager bằng việc sử dụng dependency này.
dependencies {
def work_version = "1.0.0-beta02"
implementation "android.arch.work:work-runtime-ktx:$work_version"
}
Các bạn có thể dễ dàng tìm thấy các phiên bản mới nhất của thư viện này ở đây. Để dùng Java dependency, chúng ta chỉ cần bỏ -ktx
.
Xác định những gì công việc của chúng ta làm
Bây giờ chúng ta chỉ tập chung vào một phần công việc, trước khi xâu chuỗi nhiều tasks lại với nhau. Mình sẽ đi sâu vào upload task trước. Đầu tiên, chúng ta sẽ cần tạo implementation với Worker
class. Mình sẽ gọi nó là UploadWorker
và sẽ override phương thức doWork()
.
Các Worker
:
- Xác định những gì công việc của chúng ta thực sự làm.
- Accept inputs và produce outputs. Cả input và output thể hiện như key và value pairs.
- Luôn luôn return một giá trị thể hiện các trạng thái success, failure, hoặc retry.
Dưới đây là ví dụ làm thể nào để implement một Worker
để upload image:
class UploadWorker(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {
override fun doWork(): Result {
try {
// Get the input
val imageUriInput = inputData.getString(Constants.KEY_IMAGE_URI)
// Do the work
val response = upload(imageUriInput)
// Create the output of the work
val imageResponse = response.body()
val imgLink = imageResponse.data.link
// workDataOf (part of KTX) converts a list of pairs to a [Data] object.
val outputData = workDataOf(Constants.KEY_IMAGE_URI to imgLink)
return Result.success(outputData)
} catch (e: Exception) {
return Result.failure()
}
}
fun upload(imageUri: String): Response {
TODO(“Webservice request code here”)
// Webservice request code here; note this would need to be run
// synchronously for reasons explained below.
}
}
Lưu ý:
- Input và output được truyền như
Data
, cơ bản nó là một ánh xạ của primitive types và arrays. Đối tượng data được chỉ định khá nhỏ - thực tế tổng kích thước của input/output được giới hạn bởiMAX_DATA_BYTES
. Nếu chúng ta cần đưa nhiều dữ liệu vào và ra củaWorker
thì nên put data vào một nơi khác, như Room database chẳng hạn. Như ở ví dụ trên, chúng ta chỉ truyền vào một Uri image. - Trong code thể hiện ví dụ cho việc trả về
Result.success()
vàResult.failure()
. Vẫn có một lựa chọn khác làResult.retry()
, retry công việc của chúng ta, mình sẽ đề cập sau.
Xác định work của chúng ta nên chạy như thế nào
Trong khi Worker
xác định công việc làm gì, WorkRequest
xác định như thế nào và khi nào công việc nên được run.
Dưới đây là một ví dụ về việc tạo một OneTimeWorkRequest
cho UploadWorker
. Và nó cũng có thể có một repeating PeriodicWorkRequest
.
// workDataOf (part of KTX) converts a list of pairs to a [Data] object.
val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(imageData)
.build()
WorkRequest
này có trong đối tượng imageData: Data
như một input và run ngay khi có thể.
Để yêu cầu UploadWork
không run ngay lập tức và chỉ nên run nếu device có kết nối internet. Chúng ta có thể add đối tượng Constraints
.
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
Dưới đây, một ví dụ về các constraints được hỗ trợ khác:
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(true)
.setRequiresStorageNotLow(true)
.setRequiresDeviceIdle(true)
.build()
Chúng ta đã nhắc trước đó là nếu một Worker
return Result.retry()
, WorkManager sẽ reschedule công việc. Chúng ta có thể customize backoff criteria khi tạo WorkRequest. Điều này cho phép chúng ta xác định được khi nào Worker
được retried.
Backoff criteria được định nghĩa với 2 đặc tính:
- BackoffPolicy, mặc định là theo cấp số nhân, nhưng có thể được đặt thành tuyến tính.
- Duration, mặc định là 30 giây.
// Create the Constraints
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// Define the input
val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)
// Bring it all together by creating the WorkRequest; this also sets the back off criteria
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(imageData)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build()
Running work
Code dưới đây giúp chúng ta yêu cầu WorkManager schedule cho chúng ta.
WorkManager.getInstance().enqueue(uploadWorkRequest)
Đầu tiên chúng ta cần get instance của WorkManager
, đây là một singleton có trách nhiệm thực thi công việc của chúng ta. Gọi enqueue
để bắt đầu process của tracking và scheduling work WorkManager
.
Work run như thế nào
Mặc định WorkManager
sẽ:
- Run work của chúng ta không trên main thread (off of the main thread).
- Guarantee work sẽ thực thi (Nó sẽ không quên run work của chúng ta, ngay cả chúng ta restart device hoặc app exist).
- Run theo best practices for the user’s API level.
Bây giờ chúng ta sẽ tìm hiểu sâu hơn, xem WorkManager
làm thế nào để chắc chắn rằng work sẽ run off of main thread và được đảm bảo để thực thi. WorkManager
bao gồm các thành phần:
- Internal TaskExecutor: Một single thread
Executor
xử lý tất cả các request để enqueue work. - WorkManager database: Một local database tracking tất cả thông tin và trạng thái work của chúng ta. Bao gồm trạng thái hiện tại của work, input và output từ work và bất kì constrait nào. Database này cho phép
WorkManager
đảm bảo work sẽ finish - nếu người dùng restart hoặc work bị interrupted, tất cả các thông tin chi tiết của work sẽ được pull về từ database và work có thể restart when devices boot trở lại. - WorkerFactory*: Một factory mặc định, tạo instances của các
Worker
. Chúng ta sẽ cover tại sao và như thế nào config nó trong các bài post tiếp theo. - Default Executor*: Một executor mặc định, run work của chúng ta trừ khi có một chỉ định khác. Điều này chắc chắn rằng work của chúng ta sẽ run đồng bộ và off of the main thread.
(*) Là thành phần có thể được override để có các hành vi khác nhau.
Khi chúng ta enqueue WorkRequest
:
- Internal TaskExecutor sẽ thực hiện lưu trữ
WorkRequest
info vào WorkManager database. - Lát sau, khi các
Constraints
choWorkRequest
được đáp ứng (Cái này có thể ngay lập tức), Internal TaskExecutor sẽ nói vớiWorkerFactory
tạo mộtWorker
. - Sau đó,
Executor
mặc định gọi phương thứcdoWork()
của cácWorker
ngoài main thread.
Theo cách này, công việc của chúng ta, mặc định vừa được đảm bảo để thực thi và chạy ngoài luồng chính (guaranteed to execute và run off of the main thread).
Bây giờ nếu chúng ta muốn sử dụng một số cơ chế khác ngoài Executor
mặc định để chạy công việc của mình, chúng ta có thể có các box hỗ trợ cho coroutines (CoroutineWorker
) và RxJava (RxWorker
).
Ngoài ra chúng ta có thể chỉ định chính xác work sẽ được thực thi như thế nào bằng việc sử dụng ListenableWorker
. Worker
cũng chính là implementation của ListenableWorker
để chạy work của chúng ta trên default Executor
và đồng bộ hoá. Vì vậy nếu chúng ta muốn kiểm soát hoàn toàn chiến lược phân luồng các work hoặc để run bất đồng bộ, thì chúng ta có thể kế thừa từ ListenableWorker
.
Việc WorkManager gặp rắc rối khi lưu tất cả thông tin về công việc của chúng ta vào cơ sở dữ liệu là điều làm cho nó hoàn hảo cho các tác vụ cần được đảm bảo để thực thi. Điều này làm cho WorkManager
trở nên không cần thiết cho các single task mà không cần guarantee mà chỉ cần thực thi trên background thread. Ví dụ, chúng ta download một image và muốn thay đổi color của UI dựa trên image đó. Work này nên được run off of the main thread, nhưng do nó liên quan đến UI, nên ta không cần tiếp tục nếu app bị close. Vậy nên, trong trường hợp này sẽ không dùng WorkManager
.
Using Chains for dependent work
Ví dụ, bộ lọc của chúng nhiều hơn một task vụ - chúng ta cần filter nhiều image, sau đó nén lại, và upload. Nếu chúng ta muốn chạy loạt các WorkRequest
này, từng cái một hoặc song song, thì chúng ta có thể dùng chain. Sơ đồ bên dưới ví dụ một chuỗi 3 task run song song, theo sau là một compress task và một upload task, run liên tiếp nhau:
Điều này thì cực kì dễ dàng với WorkManager
. Giả sử chúng ta đã tạo tất cả các WorkRequest
với các constraints thích hợp.
WorkManager.getInstance()
.beginWith(Arrays.asList(
filterImageOneWorkRequest,
filterImageTwoWorkRequest,
filterImageThreeWorkRequest))
.then(compressWorkRequest)
.then(uploadWorkRequest)
.enqueue()
Có 3 WorkRequest
thực thi song song. Chỉ khi cả 3 filter task kết thúc, compressWorkRequest
mới bắt đầu thực hiện, cuối cùng đến uploadWorkRequest
.
Chức năng của chuỗi đó là output của WorkRequest
này sẽ là input của WorkRequest
tiếp theo. Vì vậy, giả sử bạn đặt chính xác input và output, như mình đã làm ở trên với ví dụ UploadWorker, các giá trị này sẽ tự động được passed.
Để xử lý output từ 3 filter work request chạy song song, chúng ta có thể dùng InputMerger
, cụ thể là ArrayCreatingInputMerger
.
val compressWorkRequest = OneTimeWorkRequestBuilder<CompressWorker>()
.setInputMerger(ArrayCreatingInputMerger::class.java)
.setConstraints(constraints)
.build()
Lưu ý rằng InputMerger
được thêm vào compressWorkRequest
chứ không phải 3 filter requests được run song song.
Giả sử output của mỗi filter work request là key KEY_IMAGE_URI
được map với image URI. ArrayCreatingInputMerger
là output từ các work request chạy song song với keys tương ứng, nó tạo ra một mảng với tất cả các giá trị output, được mapped tới single key.
Input cho compressWorkRequest cuối cùng sẽ trở thành cặp “KEY_IMAGE_URI” mapped với một mảng image URIs đã được filtered.
Quan sát trạng thái của WorkRequest
Để quan sát trạng thái của WorkRequest
một cách dễ dàng nhất ta sẽ sử dụng LiveData
. LiveData
là một lifecycle-aware observable data holder .
Việc gọi getWorkInfoByIdLiveData
sẽ return LiveData
của WorkInfo
. WorkInfo
bao gồm dữ liệu output và một enum đại diện cho trạng thái của work. Khi work kết thúc thành công, State
sẽ là SUCCEEDED
. Ví dụ, chúng ta có thể tự động hiển thị hình ảnh khi work đã hoàn thành, như code bên dưới:
// In your UI (activity, fragment, etc)
WorkManager.getInstance().getWorkInfoByIdLiveData(uploadWorkRequest.id)
.observe(lifecycleOwner, Observer { workInfo ->
// Check if the current work's state is "successfully finished"
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
displayImage(workInfo.outputData.getString(KEY_IMAGE_URI))
}
})
Một số điều cần lưu ý:
- Mỗi
WorkRequest
sẽ có mộtid
duy nhất vàid
này là cách để truy cập vàoWorkInfo
. - Khả năng observe và được notified khi
WorkInfo
thay đổi là một chức năng được cung cấp bởiLiveData
.
Work cũng có một lifecycle, thể hiện bởi sự khác nhau của các State
. Khi đang observe LiveData<WorkInfo>
chúng ta sẽ thấy những trạng thái đó. Ví dụ:
Nhưng hình trên thì ta có:
BLOCKED
: State này chỉ xảy ra nếu work trong chain và không là next work trong chain.ENQUEUED
: Work vào trạng thái này ngay khi work là next work trong chain và thích hợp để run. Work này có thể vẫn đang chờ trong trên cácConstraint
để được đáp ứng.RUNNING
: Trong state này, work đang được thực hiện. Đối vớiWorker
, điều này cũng đồng nghĩa với việc phương thứcdoWork()
đã được gọi.SUCCEEDED
: Trạng thái này là trạng thái cuối cùng khidoWork()
returnResult.success()
.
Bây giờ khi work RUNNING
, chúng ta có thể gọi Result.retry()
. Điều này khiến work quay trở lại trạng thái ENQUEUED
. Ở đây thì Work cũng có thể CANCELLED
ở bất kì thời điểm nào.
Nếu work result trả về Result.failure()
thay vì success, thì state của chúng ta sẽ là FAILED
. Full flowchart của các states trong như thế này:
Để nắm rõ hơn về WorkManager
mọi người có thể xem video của WorkManager Android Developer Summit talk - 2018.
Kết luận
Trên đây là những điều cơ bản của WorkManager API. Sử dụng snippets mình vừa giới thiệu bạn có thể:
- Tạo các Workers với input và output.
- Config các
Worker
sẽ run như thế nào, bằng việc sử dụngWorkRequest
,Constraint
, starting input và back off policies. - Enqueue các
WorkRequest
. - Nắm được cơ bản
WorkManager
làm gì bên dưới. - Tạo một chain phức tạp của các work phụ thuộc lẫn nhau. Running cả tuần tự và song song.
- Quan sát
WorkRequest
status bằng việc sử dụngWorkInfo
.
Tham khảo
All rights reserved