+2

Advance WorkManager (Part II)

Trong Part I chúng ta đã tìm hiểu những kiến thức cơ bản về WorkManager. Trong phần này chúng ta sẽ tiếp tục tìm hiểu sâu hơn về WorkManager như:

  • Làm sao để tạo unique work
  • Làm thế nào để cancel Work.
  • Làm thế nào để định nghĩa các rằng buộc thực thi Work (work constraints).
  • Cơ bản về Background Task Inspector trên Android Studio.

Tạo unique work

Bây giờ bạn đã biết làm thế nào để tạo một chuỗi các Work, đã đến lúc sử dụng một tính năng mạnh mẽ khác của WorkManager: unique work sequences Thỉnh thoảng bạn chỉ muốn thực hiện một chuỗi các Work tại một thời điểm. Ví dụ, bạn thực hiện một chuỗi các Work cho việc đồng bộ data với server. Bạn chỉ muốn thực hiện việc đồng bộ data hoàn thành trước khi bắt đầu một cái chuỗi Work mới. Để làm việc này bạn sử dụng beginUniqueWork() thay cho beginWith() và bạn cung cấp một tên định danh duy nhất. Tên dịnh danh duy nhất này sẽ là định danh cho một chuỗi các Work bạn muốn thực hiện, với tên định danh này bạn có sử dụng đễ truy vấn tới chuỗi Work mà bạn muốn. Bạn muốn đảm bảo rằng khi user click vào button Start một chuỗi Work đã được start từ trước sau đó sẽ được application thay thế bằng bằng một chuỗi request mới. Thật là vô nghĩ nếu tiếp tục thực hiện các công việc với request trước đó vì dù sao application cũng thay thế nó bằng yêu cầu mới.

Trong file data/WorkManagerBlurRepository.kt, bên trong method applyBlur() bạn sẽ hoàn thành với các bước sau:

  1. Remove gọi function đã gọi beginWith() và thay thế bằng việc gọi function beginUniqueWork()
  2. Cho tham số đầu tiên tới function beginUniqueWork(), gửi vào một giá trị Constant là IMAGE_MANIPULATION_WORK_NAME
  3. Cho parameter thứ 2 existingWorkPolicy, gửi vào ExistingWorkPolicy.REPLACE.
  4. Cho parameter thứ 3 tạo một OneTimeWorkRequest mới cho CleanupWorker.

data/WorkManagerBlurRepository.kt

import androidx.work.ExistingWorkPolicy
import com.tech.codelab.blurimage.IMAGE_MANIPULATION_WORK_NAME
...
// REPLACE THIS CODE:
// var continuation = workManager.beginWith(OneTimeWorkRequest.from(CleanupWorker::class.java))
// WITH
var continuation = workManager
    .beginUniqueWork(
        IMAGE_MANIPULATION_WORK_NAME,
        ExistingWorkPolicy.REPLACE,
        OneTimeWorkRequest.from(CleanupWorker::class.java)
    )
...

Tag và update UI dựa trên trạng thái của Work

Thay đổi tiếp theo bạn sẽ làm những gì sẽ hiển thị trên application khi Work được thực thi. Bảng bên dưới hiển thị 3 method khác nhau mà bạn có thể gọi để có được thông tin của Work.

WorkInfo chứa chi tiết về trạng thái hiện tại của WorkRequest bao gồm:

  • Một trong các trạng thái của Work là BLOCKED, CANCELLED, ENQUEUED, FAILED, RUNNING, or SUCCEEDED.
  • Liệu WorkRequest đã được finish và bất kỳ data output từ Work Những method này sẽ trả về LiveData. Bạn có thể convert nó vào trong một Flow của [WorkInfo] (http://d.android.com/reference/androidx/work/WorkInfo) bằng cách gọi .asFlow() Cuối cùng bạn chỉ quan tâm tới image sau khi đã được lưu lại, bạn add một tag tới WorkRequest SaveImageToFileWorker để bạn nhận về WorkInfo của nó từ method getWorkInfosByTagLiveData(). Một cách khác có thể sử dụng method getWorkInfosForUniqueWorkLiveData() để trả về thông tin của tất 3 WorkRequest (CleanupWorker, BlurWorker, và SaveImageToFileWorker). Nhược điểm của method này là bạn cần bổ sung code để tìm cụ thể thông tin của SaveImageToFileWorker.

Trong file data/WorkManagerBlurRepository.kt bên trong method applyBlur() khi bạn tạo Work Request của SaveImageToFileWorker bạn add Tag bằng cách gọi method addTag() và gửi vào đó một giá trị constant String ví dụ trong simple là TAG_OUTPUT. data/WorkManagerBlurRepository.kt

import com.tech.codelab.blurimage.TAG_OUTPUT
...
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
    .addTag(TAG_OUTPUT) // <- Add this
    .build()

Thay vì sử dụng ID, bạn sử dụng Tag để dán nhãn Work của bạn bởi vì user có thể blur nhiều image, tất cả các WorkRequest cho việc lưu lại image sẽ có cùng một Tag nhưng không cùng ID.

Get the WorkInfo Bạn sử dụng WorkInfo để lấy về thông tin từ WorkRequest SaveImageToFileWorker để xử lý logic và quyết định việc hiển thị UI dựa trên BlurUiState ViewModel sẽ sử dụng thông tin này từ biến outputWorkInfo đã được định nghĩa trong repository.

data/WorkManagerBlurRepository.kt

...
override val outputWorkInfo: Flow<WorkInfo?> =
    workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
...

Việc gọi method getWorkInfosByTagLiveData() sẽ trả về một LiveData bạn sẽ convert nó tới Flow bằng cách gọi .asFlow()

data/WorkManagerBlurRepository.kt

import androidx.lifecycle.asFlow
import kotlinx.coroutines.flow.mapNotNull
...
override val outputWorkInfo: Flow<WorkInfo?> =
        workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow().mapNotNull {
            if (it.isNotEmpty()) it.first() else null
        }
...

Bởi vì .mapNotNull() đảm bảo rằng giá trị đã tồn tại nên bạn có thể remove ? từ Flow<WorkInfo?> data/WorkManagerBlurRepository.kt

...
    override val outputWorkInfo: Flow<WorkInfo> =
...

data/BluromaticRepository.kt

...
interface BluromaticRepository {
//    val outputWorkInfo: Flow<WorkInfo?>
    val outputWorkInfo: Flow<WorkInfo>
...

WorkInfo đã được emit như là một Flow từ repository. Tiếp theo ViewModel sẽ xử lý logic với nó.

Update giá trị BlurUiState

ui/BlurViewModel.kt

// ...
// REMOVE
// val blurUiState: StateFlow<BlurUiState> = MutableStateFlow(BlurUiState.Default)

// ADD
val blurUiState: StateFlow<BlurUiState> =
    bluromaticRepository.outputWorkInfo
// ...

Bạn cần map giá trị trong Flow tới BlurUiState phụ thuộc theo trạng thái của Work. Khi Work được finish thì giá trị blurUiStateBlurUiState.Complete(outputUri = "") Khi Work bị cancel thì giá trị của blurUiStateBlurUiState.Default Nếu không thì giá trị của blurUiStateBlurUiState.Loading

ui/BlurViewModel.kt

import androidx.work.WorkInfo
import kotlinx.coroutines.flow.map
// ...

    val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
        .map { info ->
            when {
                info.state.isFinished -> {
                    BlurUiState.Complete(outputUri = "")
                }
                info.state == WorkInfo.State.CANCELLED -> {
                    BlurUiState.Default
                }
                else -> BlurUiState.Loading
            }
        }

// ...

Bởi vì bạn chỉ quan tâm StateFlow, nên cần convert Flow tới State bằng cách gọi function .stateIn()

ui/BlurViewModel.kt

import kotlinx.coroutines.flow.stateIn
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
// ...

    val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
        .map { info ->
            when {
                info.state.isFinished -> {
                    BlurUiState.Complete(outputUri = "")
                }
                info.state == WorkInfo.State.CANCELLED -> {
                    BlurUiState.Default
                }
                else -> BlurUiState.Loading
            }
        }.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = BlurUiState.Default
        )

// ...

Update UI Trong file ui/BlurScreen.kt , bạn nhận được UI State từ variable blurUiState trong ViewModel và update lại UI . Trong compose BlurActions chúng ta sẽ hoàn với đoạn code bên dưới:

...
        Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center
    ) {
        when (blurUiState) {
            is BlurUiState.Default -> {
                Button(onClick = onStartClick) {
                    Text(text = stringResource(id = R.string.start))
                }
            }

            is BlurUiState.Loading -> {
                FilledTonalButton(onClick = onCancelClick) {
                    Text(text = stringResource(id = R.string.cancel_work))
                }
                CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(id = R.dimen.padding_small)))
            }

            is BlurUiState.Complete -> {
                Button(onClick = onStartClick) {
                    Text(text = stringResource(id = R.string.start))
                }
            }
        }
    }
...

Bây giờ bạn hãy run app và click vào button Start Bạn hãy mở cửa số Background Task Inspector (View > Tool Windows > App Inspection sau đó chọn tab Background Task Inspector). Nhìn thấy các trạng thái khác nhau tương ứng với trạng thái UI đang được hiển thị. SystemJobService là thành phần chịu trách nhiệm quản lý các Worker được thực thi. Trong khi các worker đang chạy, UI hiển thị button Cancel Work và circle progress indicator.

Sau khi các workers đã được finish, UI được update và hiển thị button Start

Hiển thị file image sau khi Blur Tiếp theo chúng ta sẽ hiển thị button See File sau khi image đã được blur. Button See File chỉ được hiển khi BlurUiStateComplete . Mở file ui/BlurScreeen.kt và di chuyển đến compose BlurActions.

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width

// ...
is BlurUiState.Complete -> {
    Button(onStartClick) { Text(stringResource(R.string.start)) }
    // Add a spacer and the new button with a "See File" label
    Spacer(modifier = Modifier.width(dimensionResource(R.dimen.padding_small)))
    FilledTonalButton({ onSeeFileClick(blurUiState.outputUri) })
    { Text(stringResource(R.string.see_file)) }
}
// ...

Chúng ta sẽ cần update lại chút xíu trong blurUiState BlurUiState được set trong ViewModel và phụ thuộc trên trạng thái (state) của WorkRequest, trong trường hợp này là blurRepository.outputWorkInfo

ui/BlurViewModel.kt

// ...
.map { info ->
    val outputImageUri = info.outputData.getString(KEY_IMAGE_URI)
    when {
        info.state.isFinished && !outputImageUri.isNullOrEmpty() -> {
            BlurUiState.Complete(outputUri = outputImageUri)
        }
        info.state == WorkInfo.State.CANCELLED -> {
// ...

Khi user click vào button See File, method onClick sẽ gọi đến function được assign cho nó. Function này được gửi vào như là một đối số của compose BlurActions()

// ...
fun BlurActions(
    blurUiState: BlurUiState,
    onStartClick: () -> Unit,
    onSeeFileClick: (String) -> Unit,
    onCancelClick: () -> Unit,
    modifier: Modifier = Modifier
)
// ...

Tạo một method cho việc hiển thị image có tên showBlurredImage(context, currentUri)

private fun showBlurredImage(context: Context, currentUri: String) {
    val uri = if (currentUri.isNotEmpty()) {
        Uri.parse(currentUri)
    } else {
        null
    }

    val actionView = Intent(Intent.ACTION_VIEW, uri)
    context.startActivity(actionView)
}

Trong compose BlurScreenContent() chúng ta update lại việc gọi compose BlurActions() như bên dưới.

// ...
fun BlurScreenContent(
    blurUiState: BlurUiState,
    blurAmountOptions: List<BlurAmount>,
    applyBlur: (Int) -> Unit,
    cancelWork: () -> Unit
) 
// ...
        BlurActions(
            blurUiState = blurUiState,
            onStartClick = { applyBlur(selectedValue) },
            onSeeFileClick = { currentUri ->
                showBlurredImage(context, currentUri)
            },
            onCancelClick = cancelWork,
            modifier = Modifier.fillMaxWidth()
        )
// ...

Bây giờ bạn chạy lại app và nhìn thấy button See File.

Cancel work

Trong phần trên, chúng ta đã add một button Cancel Work, bây giờ chúng ta sẽ thêm code để thực hiện việc làm thế nào để cancel các WorkRequest trong WorkManager. Bạn có thể cancel các công việc đang thực thi (work) bằng cách sử dụng id, tag và unique change name. Trong phần này tôi muốn cancel work bằng cách sử dụng unique chain name bởi vì tôi muốn cancel tất cả các work trong một chuỗi công việc đang thực hiện, không chỉ trong một công việc cụ thể.

Mở file data/WorkManagerBlurRepository.kt . Trong function cancelWork() gọi workManager.cancelUniqueWork(), truyền vào unique chain name là IMAGE_MANIPULATION_WORK_NAME chỉ cancel các Work đã được lên schedule với tên đó. data/WorkManagerBlurRepository.kt

override fun cancelWork() {
    workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}

Theo design principle của separation of concerns thì compose function không tương tác trực tiếp với repository. Các compose function tương tác với ViewModel , ViewModel tương tác với Repository. Theo quy tắc này bạn sẽ không cần phải thay đổi các compose function.

Mở file ui/BlurViewModel.kt và tạo một function mới là cancelWork() . Bên trong function gọi method cancelWork() trên blurRepository

ui/BlurViewModel.kt

 /**
     * Call method from repository to cancel any ongoing WorkRequest
     */
    fun cancelWork() {
        blurRepository.cancelWork()
    }

Tiếp theo chúng ta sẽ setup khi user click button Cancel Work.

Mở file ui/BlurScreen.kt và di chuyển tới compose BlurScreen()

ui/BlurScreen.kt

fun BluromaticScreen(blurViewModel: BlurViewModel = viewModel(factory = BlurViewModel.Factory)) {
    val uiState by blurViewModel.blurUiState.collectAsStateWithLifecycle()
    BluromaticTheme {
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            BluromaticScreenContent(
                blurUiState = uiState,
                blurAmountOptions = blurViewModel.blurAmount,
                applyBlur = blurViewModel::applyBlur,
                cancelWork = {}
            )
        }
    }
}

Bên trong gọi tới compose BlurScreenContent, ViewModel gọi method cancelWork() khi user click button ui/BlurScreen.kt

// ...
        BluromaticScreenContent(
            blurUiState = uiState,
            blurAmountOptions = blurViewModel.blurAmount,
            applyBlur = blurViewModel::applyBlur,
            cancelWork = blurViewModel::cancelWork
        )
// ...

Run lại app và bắt đầu blur một picture sau đó click Cancel Work. Toàn bộ chuỗi công việc bị cancel.

Sau khi bạn cancel work, chỉ button Start được hiển thị bởi vì WorkInfo.StateCANCELLED. Thay đổi này làm cho biến blurUiState được xét giá trị là BlurUiState.Default.

Background Task Inspector trong Android Studio hiển thị status là Cancelled

Các rằng buộc (constraints) cho Work

WorkManager hỗ trợ các rằng buộc (constraints) cho việc thực hiện các Work Request. Một Constraint là một yêu cầu mà bạn cần đạt được trước khi một WorkRequest được thực thi. Một vài ví dụ về các rằng buộc (constraints) là requiresDeviceIdle()requiresStorageNotLow()

  • requiresDeviceIdle() nếu được xét giá trị là true, Work sẽ chỉ được thực thi nếu device là idle
  • requiresStorageNotLow() nếu được xét giá trị là true, Work chỉ được thực thi nếu storage không là low Trong sample này chúng ta sẽ tạo một rằng buộc (constraint) khi device không ở trạng thái battery low

Mở file data/WorkManagerBlurRepository.kt và di chuyển đến method applyBlur(). Tạo một biến với tên constraints và gọi Constraints.Builder() . Sau đó gọi setRequiresBatteryNotLow() với giá true

data/WorkManagerBluromaticRepository.kt

import androidx.work.Constraints

// ...
    override fun applyBlur(blurLevel: Int) {
        // ...

        val constraints = Constraints.Builder()
            .setRequiresBatteryNotLow(true)
            .build()
// ...

Xét constraints mà chúng ta vừa tạo ở trên một WorkRequest là blurBuilder bằng cách gọi method .setConstraints() data/WorkManagerBlurRepository.kt

// ...
blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))

blurBuilder.setConstraints(constraints) // Add this code
//...

Test trên emulator Trên emulator , thay đổi Charge level trong Extended Controls tới 15% hoặc lower để mô phỏng trạng thái battery của device ở chế độ thấp.

Run lại app trên emulator và click button Start Background Task Inspector bạn sẽ thấy WorkManager không thực thi blurWorker bởi vì batter của device đang ở trạng thái low. Bây giờ bạn sẽ thay đổi giá trị của batter từ từ lên (khoảng 20%) và quan sát trong Background Task Inspector sẽ thấy rằng WorkRequest được thi.

Debug WorkManager với Background Task Inspector

Trong Android Studio có một công cụ giúp chúng ta thực hiện debug một cách trực quan, giám sát và debug các WorkRequest theo thời gian thực là Background Task Inspector View > Tool Windows > App Inspection.

**Background Task Inspector **

Các bạn có thể tham khảo source code cho phần này 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í