고급 WorkManager 및 테스트

1. 소개

WorkManager로 백그라운드 작업 Codelab에서는 WorkManager를 사용하여 (기본 스레드가 아닌) 백그라운드에서 작업을 실행하는 방법을 알아봤습니다. 이 Codelab에서는 고유 작업 보장, 작업 태그 지정, 작업 취소, 작업 제약 조건을 위한 WorkManager 기능을 계속 알아봅니다. 이 Codelab에서는 worker가 제대로 작동하고 예상 결과를 반환하는지 확인하기 위한 자동 테스트를 작성하는 방법을 알아봅니다. Android 스튜디오에서 제공하는 Background Task Inspector를 사용하여, 큐에 추가된 worker를 검사하는 방법도 알아봅니다.

빌드할 항목

이 Codelab에서는 고유 작업, 작업 태그 지정, 작업 취소, 작업 제약 조건 구현을 보장합니다. 그런 다음, WorkManager로 백그라운드 작업 Codelab에서 만든 세 worker의 기능을 확인하는 Blur-O-Matic 앱의 자동화된 UI 테스트를 작성하는 방법을 알아봅니다.

  • BlurWorker
  • CleanupWorker
  • SaveImageToFileWorker

학습할 내용

  • 고유 작업 보장
  • 작업을 취소하는 방법
  • 작업 제약 조건을 정의하는 방법
  • worker 기능을 확인하기 위한 자동 테스트를 작성하는 방법
  • 큐에 추가된 worker를 Background Task Inspector로 검사하는 방법 기본사항

필요한 항목

2. 설정

코드 다운로드

다음 링크를 클릭하면 이 Codelab의 모든 코드를 다운로드할 수 있습니다.

또는 원한다면 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 intermediate

Android 스튜디오에서 프로젝트를 엽니다.

3. 고유 작업 보장

worker를 체이닝하는 방법을 알아봤습니다. 이제 WorkManager의 또 다른 강력한 기능인 고유 작업 시퀀스를 살펴보겠습니다.

작업 체인을 한 번에 하나씩만 실행해야 하는 경우가 있습니다. 예를 들어 로컬 데이터를 서버와 동기화하는 작업 체인이 있다고 가정하겠습니다. 첫 번째 데이터 동기화가 완료된 후에 새 동기화가 시작되도록 하려면 beginWith() 대신 beginUniqueWork()를 사용하고 고유한 String 이름을 제공합니다. 이렇게 입력하면 함께 참조하고 쿼리할 수 있도록 전체 작업 요청 체인의 이름이 지정됩니다.

ExistingWorkPolicy 객체도 전달해야 합니다. 이 객체는 작업이 이미 존재하는 경우 어떻게 되는지 Android OS에 알립니다. 가능한 ExistingWorkPolicy 값은 REPLACE, KEEP, APPEND 또는 APPEND_OR_REPLACE입니다.

이 앱에서는 사용자가 현재 이미지 완료 전에 다른 이미지를 블러 처리하려는 경우 현재 이미지를 중지하고 새 이미지의 블러 처리를 시작하도록 하기 위해 REPLACE를 사용하려고 합니다.

또한 이미 작업 요청이 큐에 추가되었을 때 사용자가 Start(시작)를 클릭하면 앱이 이전 작업 요청을 새 요청으로 대체하도록 지정하려고 합니다. 앱이 이전 요청을 새 요청으로 대체하므로, 이전 요청을 계속 작업하는 것은 의미가 없습니다.

data/WorkManagerBluromaticRepository.kt 파일의 applyBlur() 메서드 내에서 다음 단계를 완료합니다.

  1. beginWith() 함수 호출을 삭제하고 beginUniqueWork() 함수 호출을 추가합니다.
  2. beginUniqueWork() 함수의 첫 번째 매개변수에 IMAGE_MANIPULATION_WORK_NAME 상수를 전달합니다.
  3. 두 번째 매개변수인 existingWorkPolicy 매개변수에 ExistingWorkPolicy.REPLACE를 전달합니다.
  4. 세 번째 매개변수의 경우 CleanupWorker의 새 OneTimeWorkRequest를 만듭니다.

data/WorkManagerBluromaticRepository.kt

import androidx.work.ExistingWorkPolicy
import com.example.bluromatic.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)
    )
...

이제 Blur-O-Matic이 한 번에 한 이미지만 블러 처리합니다.

4. 작업 상태에 따른 태그 지정 및 UI 업데이트

다음으로 할 일은 작업 실행 시 앱에 표시되는 내용을 변경하는 것입니다. 큐에 추가된 작업에 관해 반환된 정보에 따라 UI가 어떻게 변경되는지 결정됩니다.

다음 표에는 작업 정보를 가져오기 위해 호출할 수 있는 세 가지 메서드가 나와 있습니다.

유형

WorkManager 메서드

설명

ID를 사용하여 작업 가져오기

getWorkInfoByIdLiveData()

이 함수는 특정 WorkRequest의 단일 LiveData<WorkInfo>를 ID를 기준으로 반환합니다.

고유 체인 이름을 사용하여 작업 가져오기

getWorkInfosForUniqueWorkLiveData()

이 함수는 고유한 WorkRequest 체인에 있는 모든 작업의 LiveData<List<WorkInfo>>를 반환합니다.

태그를 사용하여 작업 가져오기

getWorkInfosByTagLiveData()

이 함수는 태그의 LiveData<List<WorkInfo>>를 반환합니다.

WorkInfo 객체에는 다음을 포함하여 WorkRequest의 현재 상태에 관한 세부정보가 있습니다.

이러한 메서드는 LiveData를 반환합니다. LiveData는 수명 주기를 인식하는 관찰 가능한 데이터 홀더입니다. .asFlow()를 호출하여 LiveData를 WorkInfo 객체의 Flow로 변환합니다.

최종 이미지가 저장되는 시점에 관심이 있으므로 SaveImageToFileWorker WorkRequest에 태그를 추가하여 getWorkInfosByTagLiveData() 메서드에서 WorkInfo를 가져올 수 있습니다.

또 다른 옵션은 세 WorkRequest(CleanupWorker, BlurWorker, SaveImageToFileWorker) 모두에 관한 정보를 반환하는 getWorkInfosForUniqueWorkLiveData() 메서드를 사용하는 것입니다. 이 메서드의 단점은 구체적으로 필요한 SaveImageToFileWorker 정보를 찾으려면 추가 코드가 필요하다는 점입니다.

작업 요청에 태그 지정

작업에 태그를 지정하는 작업은 data/WorkManagerBluromaticRepository.kt 파일의 applyBlur() 함수 내에서 이루어집니다.

  1. SaveImageToFileWorker 작업 요청을 만들 때 addTag() 메서드를 호출하고 String 상수 TAG_OUTPUT을 전달하여 작업에 태그를 지정합니다.

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.TAG_OUTPUT
...
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
    .addTag(TAG_OUTPUT) // <- Add this
    .build()

WorkManager ID 대신 태그를 사용하여 작업에 라벨을 지정합니다. 사용자가 여러 이미지를 블러 처리하는 경우 모든 이미지 저장 WorkRequest는 태그가 같지만 ID가 같지 않습니다.

WorkInfo 가져오기

로직에서 SaveImageToFileWorker 작업 요청의 WorkInfo 정보를 사용하여 BlurUiState에 따라 UI에 표시할 컴포저블을 결정합니다.

ViewModel은 저장소의 outputWorkInfo 변수에서 이 정보를 사용합니다.

SaveImageToFileWorker 작업 요청에 태그를 지정했으므로 이제 아래의 단계를 완료하여 정보를 가져올 수 있습니다.

  1. data/WorkManagerBluromaticRepository.kt 파일에서 workManager.getWorkInfosByTagLiveData() 메서드를 호출하여 outputWorkInfo 변수를 채웁니다.
  2. 메서드의 매개변수에 TAG_OUTPUT 상수를 전달합니다.

data/WorkManagerBluromaticRepository.kt

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

getWorkInfosByTagLiveData() 메서드를 호출하면 LiveData가 반환됩니다. LiveData는 수명 주기를 인식하는 관찰 가능한 데이터 홀더입니다. .asFlow() 함수는 LiveData를 Flow로 변환합니다.

  1. .asFlow() 함수 호출을 체이닝하여 메서드를 Flow로 변환합니다. 앱이 LiveData 대신 Kotlin Flow로 작동할 수 있도록 메서드를 변환합니다.

data/WorkManagerBluromaticRepository.kt

import androidx.lifecycle.asFlow
...
override val outputWorkInfo: Flow<WorkInfo?> =
    workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow()
...
  1. .mapNotNull() 변환 함수 호출을 체이닝하여 Flow에 값이 포함되도록 합니다.
  2. 변환 규칙의 경우 요소가 비어 있지 않으면 컬렉션의 첫 번째 항목을 선택합니다. 요소가 비어 있으면 null 값을 반환합니다. null 값인 경우 변환 함수가 삭제합니다.

data/WorkManagerBluromaticRepository.kt

import kotlinx.coroutines.flow.mapNotNull
...
    override val outputWorkInfo: Flow<WorkInfo?> =
        workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow().mapNotNull {
            if (it.isNotEmpty()) it.first() else null
        }
...
  1. .mapNotNull() 변환 함수는 값이 존재하도록 보장하므로, Flow 유형에서 ?를 안전하게 삭제할 수 있습니다. 더 이상 null을 허용하는 유형일 필요가 없기 때문입니다.

data/WorkManagerBluromaticRepository.kt

...
    override val outputWorkInfo: Flow<WorkInfo> =
...
  1. BluromaticRepository 인터페이스에서도 ?를 삭제해야 합니다.

data/BluromaticRepository.kt

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

WorkInfo 정보는 저장소에서 Flow로 내보내집니다. 그러면 이 정보를 ViewModel이 사용합니다.

BlurUiState 업데이트

ViewModel은 저장소가 outputWorkInfo Flow에서 내보낸 WorkInfo를 사용하여 blurUiState 변수의 값을 설정합니다.

UI 코드는 blurUiState 변수 값을 사용하여 표시할 컴포저블을 결정합니다.

blurUiState를 업데이트하려면 다음 단계를 완료하세요.

  1. blurUiState 변수를 저장소의 outputWorkInfo Flow로 채웁니다.

ui/BlurViewModel.kt

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

// ADD
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
// ...
  1. 그런 다음, 작업의 상태에 따라 Flow의 값을 BlurUiState 상태로 매핑해야 합니다.

작업이 완료되면 blurUiState 변수를 BlurUiState.Complete(outputUri = "")로 설정합니다.

작업이 취소되면 blurUiState 변수를 BlurUiState.Default로 설정합니다.

그 외의 경우 blurUiState 변수를 BlurUiState.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
            }
        }

// ...
  1. StateFlow에 관심이 있으므로 .stateIn() 함수 호출을 체이닝하여 Flow를 변환합니다.

.stateIn() 함수 호출에는 세 가지 인수가 필요합니다.

  1. 첫 번째 매개변수의 경우 ViewModel에 연결된 코루틴 범위인 viewModelScope를 전달합니다.
  2. 두 번째 매개변수의 경우 SharingStarted.WhileSubscribed(5_000)를 전달합니다. 이 매개변수는 공유 시작 및 중지 시점을 제어합니다.
  3. 세 번째 매개변수의 경우 상태 흐름의 초깃값인 BlurUiState.Default를 전달합니다.

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
        )

// ...

ViewModelblurUiState 변수를 통해 UI 상태 정보를 StateFlow로 노출합니다. 이 흐름은 stateIn() 함수를 호출하여 콜드 Flow를 핫 StateFlow로 변환합니다.

UI 업데이트

ui/BluromaticScreen.kt 파일에 있는 ViewModelblurUiState 변수에서 UI 상태를 가져오고 UI를 업데이트합니다.

when 블록은 앱의 UI를 제어합니다. 이 when 블록에는 3개의 BlurUiState 상태 각각에 해당하는 브랜치가 있습니다.

UI는 Row 컴포저블 내의 BlurActions 컴포저블에서 업데이트됩니다. 다음 단계를 완료합니다.

  1. Row 컴포저블 내에서 Button(onStartClick) 코드를 삭제하고 인수로 blurUiState를 사용하는 when 블록으로 바꿉니다.

ui/BluromaticScreen.kt

...
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center
    ) {
        // REMOVE
        // Button(
        //     onClick = onStartClick,
        //     modifier = Modifier.fillMaxWidth()
        // ) {
        //     Text(stringResource(R.string.start))
        // }
        // ADD
        when (blurUiState) {
        }
    }
...

앱이 열리면 기본 상태입니다. 코드에서 이 상태는 BlurUiState.Default로 표시됩니다.

  1. 다음 코드 예와 같이 when 블록 내에서 이 상태의 브랜치를 만듭니다.

ui/BluromaticScreen.kt

...
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center
    ) {
        when (blurUiState) {
            is BlurUiState.Default -> {}
        }
    }
...

기본 상태의 경우 앱에서 Start(시작) 버튼이 표시됩니다.

  1. BlurUiState.Default 상태의 onClick 매개변수의 경우 onStartClick 변수를 전달합니다. 컴포저블에 전달됩니다.
  2. stringResourceId 매개변수의 경우 문자열 리소스 ID R.string.start를 전달합니다.

ui/BluromaticScreen.kt

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

앱이 실제로 이미지를 블러 처리할 때는 BlurUiState.Loading 상태입니다. 이 상태일 때 앱은 Cancel Work(작업 취소) 버튼과 원형 진행 상태 표시기를 표시합니다.

  1. BlurUiState.Loading 상태의 버튼 onClick 매개변수의 경우 onCancelClick 변수를 전달합니다. 컴포저블에 전달됩니다.
  2. 버튼 stringResourceId 매개변수의 경우 문자열 리소스 ID R.string.cancel_work를 전달합니다.

ui/BluromaticScreen.kt

import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalButton
...
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center
    ) {
        when (blurUiState) {
            is BlurUiState.Default -> {
                Button(onStartClick) { Text(stringResource(R.string.start)) }
            }
            is BlurUiState.Loading -> {
               FilledTonalButton(onCancelClick) { Text(stringResource(R.string.cancel_work)) }
               CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(R.dimen.padding_small)))
            }
        }
    }
...

마지막으로 구성할 상태는 이미지가 블러 처리되어 저장된 후에 발생하는 BlurUiState.Complete 상태입니다. 이때 앱은 Start(시작) 버튼만 표시합니다.

  1. BlurUiState.Complete 상태의 onClick 매개변수의 경우 onStartClick 변수를 전달합니다.
  2. stringResourceId 매개변수의 경우 문자열 리소스 ID R.string.start를 전달합니다.

ui/BluromaticScreen.kt

...
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center
    ) {
        when (blurUiState) {
            is BlurUiState.Default -> {
                Button(onStartClick) { Text(stringResource(R.string.start)) }
            }
            is BlurUiState.Loading -> {
                FilledTonalButton(onCancelClick) { Text(stringResource(R.string.cancel_work)) }
                CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(R.dimen.padding_small)))
            }
            is BlurUiState.Complete -> {
                Button(onStartClick) { Text(stringResource(R.string.start)) }
            }
        }
    }
...

앱 실행

  1. 앱을 실행하고 Start(시작)를 클릭합니다.
  2. 다양한 상태와 표시되는 UI가 어떻게 상응하는지 알아보려면 Background Task Inspector 창을 참고하세요.

SystemJobService는 작업자 실행을 관리하는 구성요소입니다.

worker가 실행되는 동안 UI에 Cancel Work(작업 취소) 버튼과 원형 진행 상태 표시기가 표시됩니다.

3395cc370b580b32.png

c5622f923670cf67.png

worker가 완료되면 UI가 업데이트되어 Start(시작) 버튼이 예상대로 표시됩니다.

97252f864ea042aa.png

81ba9962a8649e70.png

5. 최종 출력 표시

이 섹션에서는 표시할 준비가 된 블러 처리된 이미지가 있으면 항상 See File(파일 보기) 라벨의 버튼이 표시되도록 앱을 구성합니다.

See File(파일 보기) 버튼 만들기

See File(파일 보기) 버튼은 BlurUiStateComplete일 때만 표시됩니다.

  1. ui/BluromaticScreen.kt 파일을 열고 BlurActions 컴포저블로 이동합니다.
  2. Start(시작) 버튼과 See File(파일 보기) 버튼 사이에 공간을 추가하려면 BlurUiState.Complete 블록 내에 Spacer 컴포저블을 추가합니다.
  3. FilledTonalButton 컴포저블을 추가합니다.
  4. onClick 매개변수의 경우 onSeeFileClick(blurUiState.outputUri)을 전달합니다.
  5. Button의 콘텐츠 매개변수에 Text 컴포저블을 추가합니다.
  6. Texttext 매개변수의 경우 문자열 리소스 ID R.string.see_file을 사용합니다.

ui/BluromaticScreen.kt

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

blurUiState 업데이트

BlurUiState 상태는 ViewModel에서 설정되며 작업 요청의 상태와 bluromaticRepository.outputWorkInfo 변수에 종속됩니다.

  1. ui/BlurViewModel.kt 파일의 map() 변환 내에서 새 변수 outputImageUri를 만듭니다.
  2. 이 새로운 변수에 outputData 데이터 객체의 저장된 이미지 URI를 채웁니다.

KEY_IMAGE_URI 키를 사용하여 이 문자열을 검색할 수 있습니다.

ui/BlurViewModel.kt

import com.example.bluromatic.KEY_IMAGE_URI

// ...
.map { info ->
    val outputImageUri = info.outputData.getString(KEY_IMAGE_URI)
    when {
// ...
  1. 작업자가 완료되고 변수가 채워지면 표시할 블러 처리된 이미지가 있음을 나타냅니다.

outputImageUri.isNullOrEmpty()를 호출하여 이 변수가 채워졌는지 확인할 수 있습니다.

  1. isFinished 브랜치를 업데이트하여 변수가 채워졌는지 확인한 후 outputImageUri 변수를 BlurUiState.Complete 데이터 객체에 전달합니다.

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 -> {
// ...

See File 클릭 이벤트 코드 만들기

사용자가 See File(파일 보기) 버튼을 클릭하면 onClick 핸들러가 할당된 함수를 호출합니다. 이 함수는 BlurActions() 컴포저블 호출 시 인수로 전달됩니다.

이 함수의 목적은 URI에서 저장된 이미지를 표시하는 것입니다. showBlurredImage() 도우미 함수를 호출하고 URI를 전달합니다. 도우미 함수는 인텐트를 만들고 이를 사용하여 저장된 이미지를 표시하는 새 활동을 시작합니다.

  1. ui/BluromaticScreen.kt 파일을 엽니다.
  2. BluromaticScreenContent() 함수의 구성 가능한 BlurActions() 함수 호출에서 currentUri라는 단일 매개변수를 사용하는 onSeeFileClick 매개변수에 관한 람다 함수를 만들기 시작합니다. 이 방식은 저장된 이미지의 URI를 저장합니다.

ui/BluromaticScreen.kt

// ...
BlurActions(
    blurUiState = blurUiState,
    onStartClick = { applyBlur(selectedValue) },
    onSeeFileClick = { currentUri ->
    },
    onCancelClick = { cancelWork() },
    modifier = Modifier.fillMaxWidth()
)
// ...
  1. 람다 함수의 본문 내에서 showBlurredImage() 도우미 함수를 호출합니다.
  2. 첫 번째 매개변수의 경우 context 변수를 전달합니다.
  3. 두 번째 매개변수의 경우 currentUri 변수를 전달합니다.

ui/BluromaticScreen.kt

// ...
BlurActions(
    blurUiState = blurUiState,
    onStartClick = { applyBlur(selectedValue) },
    // New lambda code runs when See File button is clicked
    onSeeFileClick = { currentUri ->
        showBlurredImage(context, currentUri)
    },
    onCancelClick = { cancelWork() },
    modifier = Modifier.fillMaxWidth()
)
// ...

앱 실행

앱을 실행합니다. 이제 클릭 가능한 새 See File(파일 보기) 버튼이 표시됩니다. 이 버튼을 클릭하면 저장된 파일로 이동합니다.

9d76d5d7f231c6b6.png

926e532cc24a0d4f.png

6. 작업 취소

5cec830cc8ef647e.png

이전에 Cancel Work(작업 취소) 버튼을 추가했으므로 이제 코드를 추가하여 버튼의 작업을 실행할 수 있습니다. WorkManager를 사용하면 ID, 태그, 고유 체인 이름을 사용하여 작업을 취소할 수 있습니다.

이 경우 특정 단계뿐만 아니라 체인의 모든 작업을 취소하려고 하므로 고유 체인 이름으로 작업을 취소하는 것이 좋습니다.

이름으로 작업 취소

  1. data/WorkManagerBluromaticRepository.kt 파일을 엽니다.
  2. cancelWork() 함수에서 workManager.cancelUniqueWork() 함수를 호출합니다.
  3. 고유 체인 이름 IMAGE_MANIPULATION_WORK_NAME을 전달하여 호출이 해당 이름의 예약된 작업만 취소하도록 합니다.

data/WorkManagerBluromaticRepository.kt

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

관심사 분리의 설계 원칙에 따라 구성 가능한 함수는 저장소와 직접 상호작용해서는 안 됩니다. 구성 가능한 함수는 ViewModel과 상호작용하고, ViewModel은 저장소와 상호작용합니다.

이 접근 방법은 좋은 설계 원칙입니다. 구성 가능한 함수가 직접 상호작용하지 않으므로 저장소를 변경해도 구성 가능한 함수를 변경할 필요가 없기 때문입니다.

  1. ui/BlurViewModel.kt 파일을 엽니다.
  2. cancelWork()라는 새 함수를 만들어 작업을 취소합니다.
  3. 함수 내의 bluromaticRepository 객체에서 cancelWork() 메서드를 호출합니다.

ui/BlurViewModel.kt

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

Cancel Work 클릭 이벤트 설정

  1. ui/BluromaticScreen.kt 파일을 엽니다.
  2. 구성 가능한 BluromaticScreen() 함수로 이동합니다.

ui/BluromaticScreen.kt

fun BluromaticScreen(blurViewModel: BlurViewModel = viewModel(factory = BlurViewModel.Factory)) {
    val uiState by blurViewModel.blurUiState.collectAsStateWithLifecycle()
    val layoutDirection = LocalLayoutDirection.current
    Surface(
        modifier = Modifier
            .fillMaxSize()
            .statusBarsPadding()
            .padding(
                start = WindowInsets.safeDrawing
                    .asPaddingValues()
                    .calculateStartPadding(layoutDirection),
                end = WindowInsets.safeDrawing
                    .asPaddingValues()
                    .calculateEndPadding(layoutDirection)
            )
    ) {
        BluromaticScreenContent(
            blurUiState = uiState,
            blurAmountOptions = blurViewModel.blurAmount,
            applyBlur = blurViewModel::applyBlur,
            cancelWork = {},
            modifier = Modifier
                .verticalScroll(rememberScrollState())
                .padding(dimensionResource(R.dimen.padding_medium))
        )
    }
}

사용자가 버튼을 클릭하면 BluromaticScreenContent 컴포저블 호출 내에서 ViewModel의 cancelWork() 메서드가 실행되도록 하겠습니다.

  1. cancelWork 매개변수에 blurViewModel::cancelWork 값을 할당합니다.

ui/BluromaticScreen.kt

// ...
        BluromaticScreenContent(
            blurUiState = uiState,
            blurAmountOptions = blurViewModel.blurAmount,
            applyBlur = blurViewModel::applyBlur,
            cancelWork = blurViewModel::cancelWork,
            modifier = Modifier
                .verticalScroll(rememberScrollState())
                .padding(dimensionResource(R.dimen.padding_medium))
        )
// ...

앱 실행 및 작업 취소

앱을 실행합니다. 문제 없이 컴파일됩니다. 사진 블러 처리를 시작한 다음 Cancel Work(작업 취소)를 클릭합니다. 전체 체인이 취소됩니다.

81ba9962a8649e70.png

작업을 취소한 후에는 WorkInfo.StateCANCELLED이므로 Start(시작) 버튼만 표시됩니다. 이러한 변경에 따라 blurUiState 변수가 BlurUiState.Default로 설정됩니다. 그러면 UI가 초기 상태로 재설정되고 Start(시작) 버튼만 표시됩니다.

Background Task Inspector에 예상대로 Cancelled(취소됨) 상태가 표시됩니다.

7656dd320866172e.png

7. 작업 제약 조건

마지막으로 WorkManagerConstraints를 지원합니다. 제약 조건은 WorkRequest를 실행하기 전에 충족해야 하는 요구사항입니다.

제약 조건의 예로는 requiresDeviceIdle()requiresStorageNotLow()가 있습니다.

  • requiresDeviceIdle() 제약 조건의 경우 true 값이 전달되면 기기가 유휴 상태일 때만 작업이 실행됩니다.
  • requiresStorageNotLow() 제약 조건의 경우 true 값이 전달되면 저장용량이 부족하지 않을 때만 작업이 실행됩니다.

Blur-O-Matic의 경우 blurWorker 작업 요청을 실행하기 전에 기기의 배터리 잔량 수준이 낮아서는 안 된다는 제약 조건을 추가합니다. 이 제약 조건을 사용하면 작업 요청이 지연되고 기기의 배터리가 부족하지 않은 경우에만 실행됩니다.

배터리 부족 상태 아님 조건 만들기

data/WorkManagerBluromaticRepository.kt 파일에서 다음 단계를 완료합니다.

  1. applyBlur() 메서드로 이동합니다.
  2. continuation 변수를 선언하는 코드 뒤에 constraints라는 새 변수를 만듭니다. 이 변수는 생성되는 제약 조건의 Constraints 객체를 포함합니다.
  3. Constraints.Builder() 함수를 호출하여 Constraints 객체용 빌더를 만들고 새 변수를 할당합니다.

data/WorkManagerBluromaticRepository.kt

import androidx.work.Constraints

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

        val constraints = Constraints.Builder()
// ...
  1. setRequiresBatteryNotLow() 메서드를 호출에 체이닝하고 true 값을 전달하여 기기의 배터리가 부족하지 않을 때만 WorkRequest가 실행되도록 합니다.

data/WorkManagerBluromaticRepository.kt

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

        val constraints = Constraints.Builder()
            .setRequiresBatteryNotLow(true)
// ...
  1. .build() 메서드 호출을 체이닝하여 객체를 빌드합니다.

data/WorkManagerBluromaticRepository.kt

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

        val constraints = Constraints.Builder()
            .setRequiresBatteryNotLow(true)
            .build()
// ...
  1. blurBuilder 작업 요청에 제약 조건 객체를 추가하려면 .setConstraints() 메서드 호출을 체이닝하고 제약 조건 객체를 전달합니다.

data/WorkManagerBluromaticRepository.kt

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

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

에뮬레이터로 테스트

  1. 에뮬레이터에서 Extended Controls 창의 Charge level을 15% 이하로 변경하여 배터리 부족 시나리오를 시뮬레이션합니다. Charger connectionAC charger로, Battery statusNot charging으로 변경합니다.

9b0084cb6e1a8672.png

  1. 앱을 실행하고 Start(시작)를 클릭하여 이미지 블러 처리를 시작합니다.

에뮬레이터의 배터리 잔량 수준이 낮음으로 설정되어 있으므로 WorkManager는 제약 조건으로 인해 blurWorker 작업 요청을 실행하지 않습니다. 큐에 추가되지만 제약 조건이 충족될 때까지 지연됩니다. Background Task Inspector 탭에서 이 지연을 확인할 수 있습니다.

7518cf0353d04f12.png

  1. 실행되지 않았음을 확인한 후 배터리 잔량 수준을 천천히 높입니다.

배터리 잔량 수준이 약 25%에 도달한 후에는 제약 조건이 충족되고 지연 작업이 실행됩니다. 이 결과는 Background Task Inspector 탭에 표시됩니다.

ab189db49e7b8997.png

8. 작업자 구현 테스트 작성

WorkManager를 테스트하는 방법

작업자용 테스트를 작성하고 WorkManager API를 사용하여 테스트하는 것은 직관적이지 않을 수 있습니다. 작업자에서 실행된 작업은 UI에 직접 도달하지 않으며 엄밀히 말하면 비즈니스 로직입니다. 일반적으로 로컬 단위 테스트를 사용하여 비즈니스 로직을 테스트합니다. 그러나 'WorkManager로 백그라운드 작업' Codelab에서 알아본 것처럼 WorkManger에는 Android Context 실행이 필요합니다. Context는 기본적으로 로컬 단위 테스트에서 사용할 수 없습니다. 따라서 테스트할 직접적인 UI 요소가 없더라도 UI 테스트로 작업자 테스트를 해야 합니다.

종속 항목 설정

프로젝트에 Gradle 종속 항목 3개를 추가해야 합니다. 처음의 두 종속 항목은 UI 테스트에 JUnit 및 Espresso를 사용하도록 설정합니다. 세 번째 종속 항목은 작업 테스트 API를 제공합니다.

app/build.gradle.kts

dependencies {
    // Espresso
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    // Junit
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    // Work testing
    androidTestImplementation("androidx.work:work-testing:2.8.1")
}

앱에서 최신 안정화 버전work-runtime-ktx를 사용해야 합니다. 버전을 변경하는 경우에는 Sync Now(지금 동기화)를 클릭하여 업데이트된 Gradle 파일로 프로젝트를 동기화합니다.

테스트 클래스 만들기

  1. app > src 디렉터리에 UI 테스트용 디렉터리를 만듭니다. a7768e9b6ea994d3.png

20cc54de1756c884.png

  1. androidTest/java 디렉터리에 WorkerInstrumentationTest라는 새 Kotlin 클래스를 만듭니다.

CleanupWorker 테스트 작성

단계에 따라, CleanupWorker 구현을 확인하는 테스트를 작성합니다. 안내에 따라 이 인증을 직접 구현해 보세요. 솔루션은 단계 마지막에 제공됩니다.

  1. WorkerInstrumentationTest.kt에서 Context 인스턴스를 보유할 lateinit 변수를 만듭니다.
  2. @Before 주석이 지정된 setUp() 메서드를 만듭니다.
  3. setUp() 메서드에서 ApplicationProvider의 애플리케이션 컨텍스트로 lateinit 컨텍스트 변수를 초기화합니다.
  4. cleanupWorker_doWork_resultSuccess()라는 테스트 함수를 만듭니다.
  5. cleanupWorker_doWork_resultSuccess() 테스트에서 CleanupWorker 인스턴스를 만듭니다.

WorkerInstrumentationTest.kt

class WorkerInstrumentationTest {
   private lateinit var context: Context

   @Before
   fun setUp() {
       context = ApplicationProvider.getApplicationContext()
   }

   @Test
   fun cleanupWorker_doWork_resultSuccess() {
   }
}

Blur-O-Matic 앱을 작성할 때는 OneTimeWorkRequestBuilder를 사용하여 작업자를 만듭니다. 작업자를 테스트하려면 다른 작업 빌더가 필요합니다. WorkManager API는 다음과 같은 2가지 빌더를 제공합니다.

두 빌더 모두 작업자의 비즈니스 로직을 테스트할 수 있습니다. CleanupWorker, BlurWorker, SaveImageToFileWorker와 같은 CoroutineWorkers의 경우 테스트에 TestListenableWorkerBuilder를 사용합니다. 코루틴의 스레딩 복잡성을 처리하기 때문입니다.

  1. CoroutineWorker는 코루틴 사용 시 비동기식으로 실행됩니다. 작업자를 동시에 실행하려면 runBlocking을 사용하세요. 시작할 빈 람다 본문이 제공되지만, 작업자를 큐에 추가하는 대신 runBlocking을 사용하여 작업자를 doWork()에 직접 지시할 수 있습니다.

WorkerInstrumentationTest.kt

class WorkerInstrumentationTest {
   private lateinit var context: Context

   @Before
   fun setUp() {
       context = ApplicationProvider.getApplicationContext()
   }

   @Test
   fun cleanupWorker_doWork_resultSuccess() {
       val worker = TestListenableWorkerBuilder<CleanupWorker>(context).build()
       runBlocking {
       }
   }
}
  1. runBlocking의 람다 본문에서 5단계에 만든 CleanupWorker 인스턴스에서 doWork()를 호출하고 값으로 저장합니다.

CleanupWorker가 Blur-O-Matic 앱의 파일 구조에 저장된 PNG 파일을 삭제한다는 점을 기억하실 것입니다. 이 프로세스에는 파일 입력/출력이 포함됩니다. 즉, 파일 삭제를 시도하는 동안 예외가 발생할 수 있습니다. 따라서 파일 삭제 시도는 try 블록에 래핑됩니다.

CleanupWorker.kt

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

try 블록 끝에 Result.success()가 반환됩니다. 코드가 Result.success()에 도달하면 파일 디렉터리에 액세스하는 데 오류가 없습니다.

이제 작업자의 성공을 나타내는 어설션을 만들 차례입니다.

  1. 작업자의 결과가 ListenableWorker.Result.success()라고 어설션합니다.

다음 솔루션 코드를 살펴보세요.

WorkerInstrumentationTest.kt

class WorkerInstrumentationTest {
   private lateinit var context: Context

   @Before
   fun setUp() {
       context = ApplicationProvider.getApplicationContext()
   }

   @Test
   fun cleanupWorker_doWork_resultSuccess() {
       val worker = TestListenableWorkerBuilder<CleanupWorker>(context).build()
       runBlocking {
           val result = worker.doWork()
           assertTrue(result is ListenableWorker.Result.Success)
       }
   }
}

BlurWorker 테스트 작성

다음 단계에 따라, BlurWorker 구현을 확인하는 테스트를 작성합니다. 안내에 따라 이 인증을 직접 구현해 보세요. 솔루션은 단계 마지막에 제공됩니다.

  1. WorkerInstrumentationTest.kt에서 blurWorker_doWork_resultSuccessReturnsUri()라는 새 테스트 함수를 만듭니다.

BlurWorker에는 처리할 이미지가 필요합니다. 따라서 BlurWorker 인스턴스를 빌드하려면 이러한 이미지가 포함된 입력 데이터가 필요합니다.

  1. 테스트 함수 외부에서 모의 URI 입력을 만듭니다. 모의 URI는 키와 URI 값을 포함하는 쌍입니다. 이 키-값 쌍에 다음 예시 코드를 사용합니다.
KEY_IMAGE_URI to "android.resource://com.example.bluromatic/drawable/android_cupcake"
  1. blurWorker_doWork_resultSuccessReturnsUri() 함수 내에 BlurWorker를 빌드하고 setInputData() 메서드를 통해 작업 데이터로 만든 모의 URI 입력을 전달해야 합니다.

CleanupWorker 테스트와 마찬가지로 runBlocking 내부의 작업자 구현을 호출해야 합니다.

  1. runBlocking 블록을 만듭니다.
  2. runBlocking 블록 내에서 doWork()를 호출합니다.

CleanupWorker와 달리 BlurWorker에는 테스트용으로 사용 가능한 출력 데이터가 있습니다.

  1. 출력 데이터에 액세스하려면 doWork()의 결과에서 URI를 추출하세요.

WorkerInstrumentationTest.kt

@Test
fun blurWorker_doWork_resultSuccessReturnsUri() {
    val worker = TestListenableWorkerBuilder<BlurWorker>(context)
        .setInputData(workDataOf(mockUriInput))
        .build()
    runBlocking {
        val result = worker.doWork()
        val resultUri = result.outputData.getString(KEY_IMAGE_URI)
    }
}
  1. 작업자의 성공을 나타내는 어설션을 만듭니다. 예를 들어 BlurWorker의 다음 코드를 살펴보겠습니다.

BlurWorker.kt

val resourceUri = inputData.getString(KEY_IMAGE_URI)
val blurLevel = inputData.getInt(BLUR_LEVEL, 1)

...
val picture = BitmapFactory.decodeStream(
    resolver.openInputStream(Uri.parse(resourceUri))
)

val output = blurBitmap(picture, blurLevel)

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

val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

Result.success(outputData)
...

BlurWorker는 입력 데이터에서 URI 및 흐림 수준을 가져와 임시 파일을 만듭니다. 작업이 성공하면 URI가 포함된 키-값 쌍이 반환됩니다. 출력 콘텐츠가 올바른지 확인하려면 출력 데이터에 KEY_IMAGE_URI 키가 포함되어 있음을 나타내는 어설션을 만듭니다.

  1. 출력 데이터에 "file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-" 문자열로 시작하는 URI가 포함되어 있음을 나타내는 어설션을 만듭니다.
  1. 다음 솔루션 코드와 비교하여 테스트를 확인하세요.

WorkerInstrumentationTest.kt

    @Test
    fun blurWorker_doWork_resultSuccessReturnsUri() {
        val worker = TestListenableWorkerBuilder<BlurWorker>(context)
            .setInputData(workDataOf(mockUriInput))
            .build()
        runBlocking {
            val result = worker.doWork()
            val resultUri = result.outputData.getString(KEY_IMAGE_URI)
            assertTrue(result is ListenableWorker.Result.Success)
            assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI))
            assertTrue(
                resultUri?.startsWith("file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-")
                    ?: false
            )
        }
    }

SaveImageToFileWorker 테스트 작성

SaveImageToFileWorker는 이름대로 디스크에 파일을 씁니다. WorkManagerBluromaticRepository에서 SaveImageToFileWorkerBlurWorker 뒤에 연속으로 WorkManager에 추가합니다. 따라서 동일한 입력 데이터를 가집니다. 입력 데이터에서 URI를 가져와서 비트맵을 만든 다음 이 비트맵을 디스크에 파일로 씁니다. 작업이 성공하면 결과적으로 이미지 URL이 출력됩니다. SaveImageToFileWorker 테스트는 BlurWorker 테스트와 매우 비슷하며, 유일한 차이점은 출력 데이터입니다.

SaveImageToFileWorker 테스트를 직접 작성할 수 있는지 확인하세요. 완료되면 아래의 솔루션을 확인할 수 있습니다. BlurWorker 테스트에 사용한 접근 방식을 떠올려 보세요.

  1. 작업자를 빌드하고 입력 데이터를 전달합니다.
  2. runBlocking 블록을 만듭니다.
  3. 작업자에서 doWork()를 호출합니다.
  4. 결과가 성공적인지 확인합니다.
  5. 출력에 올바른 키와 값이 있는지 확인합니다.

솔루션은 다음과 같습니다.

@Test
fun saveImageToFileWorker_doWork_resultSuccessReturnsUrl() {
    val worker = TestListenableWorkerBuilder<SaveImageToFileWorker>(context)
        .setInputData(workDataOf(mockUriInput))
        .build()
    runBlocking {
        val result = worker.doWork()
        val resultUri = result.outputData.getString(KEY_IMAGE_URI)
        assertTrue(result is ListenableWorker.Result.Success)
        assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI))
        assertTrue(
            resultUri?.startsWith("content://media/external/images/media/")
                ?: false
        )
    }
}

9. Background Task Inspector로 WorkManager 디버그

작업자 검사

자동화된 테스트는 작업자의 기능을 확인하는 좋은 방법입니다. 그러나 Worker를 디버그할 때는 그리 유용하지 않습니다. 다행히 Android 스튜디오에는 작업자를 실시간으로 시각화하고 모니터링하고 디버그할 수 있는 도구가 있습니다. Background Task Inspector는 API 수준 26 이상을 실행하는 에뮬레이터 및 기기에서 작동합니다.

이 섹션에서는 Background Task InspectorBlur-O-Matic의 작업자를 검사하기 위해 제공하는 몇 가지 기능을 알아봅니다.

  1. 기기나 에뮬레이터에서 Blur-O-Matic 앱을 실행합니다.
  2. View > Tool Windows > App Inspection(보기 > 도구 창 > 앱 검사)으로 이동합니다.

798f10dfd8d74bb1.png

  1. Background Task Inspector 탭을 선택합니다.

d601998f3754e793.png

  1. 필요한 경우 드롭다운 메뉴에서 기기와 실행 중인 프로세스를 선택합니다.

예시 이미지에서 프로세스는 com.example.bluromatic입니다. 자동으로 프로세스가 선택될 수도 있습니다. 프로세스가 잘못 선택된 경우, 변경할 수 있습니다.

6428a2ab43fc42d1.png

  1. Workers 드롭다운 메뉴를 클릭합니다. 현재 실행 중인 worker가 없습니다. 이미지를 블러 처리하려고 시도하지 않았기 때문입니다.

cf8c466b3fd7fed1.png

  1. 앱에서 More blurred(블러가 더 처리됨)를 선택하고 Start(시작)를 클릭합니다. Workers 드롭다운에 일부 콘텐츠가 즉시 표시됩니다.

이제 Workers 드롭다운에 다음과 같이 표시됩니다.

569a8e0c1c6993ce.png

worker 테이블에는 worker의 이름, 서비스(이 경우 SystemJobService), 각각의 상태, 타임스탬프가 표시됩니다. 이전 단계의 스크린샷에서 BlurWorkerCleanupWorker가 작업을 성공적으로 완료했음을 알 수 있습니다.

검사기를 사용하여 작업을 취소할 수도 있습니다.

  1. 큐에 있는 작업자를 선택하고 툴바에서 Cancel Selected Worker(선택한 작업자 취소) 7108c2a82f64b348.png를 클릭합니다.

작업 세부정보 검사

  1. Workers 테이블에서 worker를 클릭합니다. 97eac5ad23c41127.png

그러면 Task Details(작업 세부정보) 창이 표시됩니다.

9d4e17f7d4afa6bd.png

  1. Task Details(작업 세부정보)에 표시된 정보를 검토합니다. 59fa1bf4ad8f4d8d.png

세부정보에 다음 카테고리가 표시됩니다.

  • Description: 이 섹션에는 정규화된 패키지가 있는 작업자 클래스 이름과 할당된 태그, 이 작업자의 UUID가 나열됩니다.
  • Execution: 이 섹션에는 작업자의 제약 조건(있는 경우)과 실행 빈도, 상태, 이 작업자를 만들고 대기열에 추가한 클래스가 표시됩니다. BlurWorker에는 배터리가 부족할 때 실행이 차단되는 제약 조건이 있습니다. 제약 조건이 있는 작업자를 검사하면 이 섹션에 표시됩니다.
  • WorkContuniation: 이 섹션에는 이 작업자가 작업 체인에서 어디에 있는지 표시됩니다. 작업 체인에 있는 다른 작업자의 세부정보를 확인하려면 UUID를 클릭하세요.
  • Results: 이 섹션에는 선택한 작업자의 시작 시간과 재시도 횟수, 출력 데이터가 표시됩니다.

그래프 뷰

Blur-O-Matic에서 작업자가 체이닝된다는 점을 떠올려 보세요. Background Task Inspector는 작업자 종속 항목을 시각적으로 나타내는 그래프 뷰를 제공합니다.

Background Task Inspector 창 모서리에 있는 두 버튼으로 Show Graph View(그래프 뷰 보기)와 Show List View(목록 뷰 보기) 간에 전환할 수 있습니다.

4cd96a8b2773f466.png

  1. Show Graph View(그래프 뷰 보기) 6f871bb00ad8b11a.png를 클릭합니다.

ece206da18cfd1c9.png

그래프 뷰는 Blur-O-Matic 앱에 구현된 worker 종속 항목을 정확하게 나타냅니다.

  1. Show List View(목록 뷰 보기) 669084937ea340f5.png를 클릭하여 그래프 뷰를 종료합니다.

추가 기능

Blur-O-Matic 앱은 백그라운드 작업 완료를 위한 작업자만 구현합니다. 그러나 Background Task Inspector에 관한 문서에서 다른 유형의 백그라운드 작업을 검사하는 데 사용할 수 있는 도구를 자세히 알아볼 수 있습니다.

10. 솔루션 코드 가져오기

완료된 Codelab의 코드를 다운로드하려면 다음 명령어를 사용하면 됩니다.

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

또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

이 Codelab의 솔루션 코드는 GitHub에서 확인하세요.

11. 마무리

축하합니다. 추가적인 WorkManger 기능에 관해 알아보고 Blur-O-Matic 작업자용 자동 테스트를 작성했으며 Background Task Inspector를 사용하여 검사했습니다. 이 Codelab에서 배운 내용은 다음과 같습니다.

  • 고유 WorkRequest 체인 이름 지정
  • WorkRequest 태그 지정
  • WorkInfo에 따른 UI 업데이트
  • WorkRequest 취소
  • WorkRequest에 제약 조건 추가
  • WorkManager 테스트 API
  • 작업자 구현 테스트에 접근하는 방법
  • CoroutineWorker를 테스트하는 방법
  • 수동으로 작업자를 검사하고 작업자의 기능을 확인하는 방법