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로 검사하는 방법 기본사항
필요한 항목
- Android 스튜디오의 최신 안정화 버전
- WorkManager로 백그라운드 작업 Codelab 완료
- Android 기기 또는 에뮬레이터
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()
메서드 내에서 다음 단계를 완료합니다.
beginWith()
함수 호출을 삭제하고beginUniqueWork()
함수 호출을 추가합니다.beginUniqueWork()
함수의 첫 번째 매개변수에IMAGE_MANIPULATION_WORK_NAME
상수를 전달합니다.- 두 번째 매개변수인
existingWorkPolicy
매개변수에ExistingWorkPolicy.REPLACE
를 전달합니다. - 세 번째 매개변수의 경우
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를 사용하여 작업 가져오기 | 이 함수는 특정 WorkRequest의 단일 LiveData<WorkInfo>를 ID를 기준으로 반환합니다. | |
고유 체인 이름을 사용하여 작업 가져오기 | 이 함수는 고유한 WorkRequest 체인에 있는 모든 작업의 LiveData<List<WorkInfo>>를 반환합니다. | |
태그를 사용하여 작업 가져오기 | 이 함수는 태그의 LiveData<List<WorkInfo>>를 반환합니다. |
WorkInfo
객체에는 다음을 포함하여 WorkRequest
의 현재 상태에 관한 세부정보가 있습니다.
- 작업의 상태(
BLOCKED
,CANCELLED
,ENQUEUED
,FAILED
,RUNNING
,SUCCEEDED
중에서) WorkRequest
가 완료된 경우 작업의 모든 출력 데이터
이러한 메서드는 LiveData를 반환합니다. LiveData는 수명 주기를 인식하는 관찰 가능한 데이터 홀더입니다. .asFlow()
를 호출하여 LiveData를 WorkInfo
객체의 Flow로 변환합니다.
최종 이미지가 저장되는 시점에 관심이 있으므로 SaveImageToFileWorker
WorkRequest에 태그를 추가하여 getWorkInfosByTagLiveData()
메서드에서 WorkInfo를 가져올 수 있습니다.
또 다른 옵션은 세 WorkRequest(CleanupWorker
, BlurWorker
, SaveImageToFileWorker
) 모두에 관한 정보를 반환하는 getWorkInfosForUniqueWorkLiveData()
메서드를 사용하는 것입니다. 이 메서드의 단점은 구체적으로 필요한 SaveImageToFileWorker
정보를 찾으려면 추가 코드가 필요하다는 점입니다.
작업 요청에 태그 지정
작업에 태그를 지정하는 작업은 data/WorkManagerBluromaticRepository.kt
파일의 applyBlur()
함수 내에서 이루어집니다.
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
작업 요청에 태그를 지정했으므로 이제 아래의 단계를 완료하여 정보를 가져올 수 있습니다.
data/WorkManagerBluromaticRepository.kt
파일에서workManager.getWorkInfosByTagLiveData()
메서드를 호출하여outputWorkInfo
변수를 채웁니다.- 메서드의 매개변수에
TAG_OUTPUT
상수를 전달합니다.
data/WorkManagerBluromaticRepository.kt
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
...
getWorkInfosByTagLiveData()
메서드를 호출하면 LiveData가 반환됩니다. LiveData는 수명 주기를 인식하는 관찰 가능한 데이터 홀더입니다. .asFlow()
함수는 LiveData를 Flow로 변환합니다.
.asFlow()
함수 호출을 체이닝하여 메서드를 Flow로 변환합니다. 앱이 LiveData 대신 Kotlin Flow로 작동할 수 있도록 메서드를 변환합니다.
data/WorkManagerBluromaticRepository.kt
import androidx.lifecycle.asFlow
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow()
...
.mapNotNull()
변환 함수 호출을 체이닝하여 Flow에 값이 포함되도록 합니다.- 변환 규칙의 경우 요소가 비어 있지 않으면 컬렉션의 첫 번째 항목을 선택합니다. 요소가 비어 있으면 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
}
...
.mapNotNull()
변환 함수는 값이 존재하도록 보장하므로, Flow 유형에서?
를 안전하게 삭제할 수 있습니다. 더 이상 null을 허용하는 유형일 필요가 없기 때문입니다.
data/WorkManagerBluromaticRepository.kt
...
override val outputWorkInfo: Flow<WorkInfo> =
...
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
를 업데이트하려면 다음 단계를 완료하세요.
blurUiState
변수를 저장소의outputWorkInfo
Flow로 채웁니다.
ui/BlurViewModel.kt
// ...
// REMOVE
// val blurUiState: StateFlow<BlurUiState> = MutableStateFlow(BlurUiState.Default)
// ADD
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
// ...
- 그런 다음, 작업의 상태에 따라 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
}
}
// ...
- StateFlow에 관심이 있으므로
.stateIn()
함수 호출을 체이닝하여 Flow를 변환합니다.
.stateIn()
함수 호출에는 세 가지 인수가 필요합니다.
- 첫 번째 매개변수의 경우 ViewModel에 연결된 코루틴 범위인
viewModelScope
를 전달합니다. - 두 번째 매개변수의 경우
SharingStarted.WhileSubscribed(5_000)
를 전달합니다. 이 매개변수는 공유 시작 및 중지 시점을 제어합니다. - 세 번째 매개변수의 경우 상태 흐름의 초깃값인
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
)
// ...
ViewModel
은 blurUiState
변수를 통해 UI 상태 정보를 StateFlow
로 노출합니다. 이 흐름은 stateIn()
함수를 호출하여 콜드 Flow
를 핫 StateFlow
로 변환합니다.
UI 업데이트
ui/BluromaticScreen.kt
파일에 있는 ViewModel
의 blurUiState
변수에서 UI 상태를 가져오고 UI를 업데이트합니다.
when
블록은 앱의 UI를 제어합니다. 이 when
블록에는 3개의 BlurUiState
상태 각각에 해당하는 브랜치가 있습니다.
UI는 Row
컴포저블 내의 BlurActions
컴포저블에서 업데이트됩니다. 다음 단계를 완료합니다.
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
로 표시됩니다.
- 다음 코드 예와 같이
when
블록 내에서 이 상태의 브랜치를 만듭니다.
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {}
}
}
...
기본 상태의 경우 앱에서 Start(시작) 버튼이 표시됩니다.
BlurUiState.Default
상태의onClick
매개변수의 경우onStartClick
변수를 전달합니다. 컴포저블에 전달됩니다.stringResourceId
매개변수의 경우 문자열 리소스 IDR.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(작업 취소) 버튼과 원형 진행 상태 표시기를 표시합니다.
BlurUiState.Loading
상태의 버튼onClick
매개변수의 경우onCancelClick
변수를 전달합니다. 컴포저블에 전달됩니다.- 버튼
stringResourceId
매개변수의 경우 문자열 리소스 IDR.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(시작) 버튼만 표시합니다.
BlurUiState.Complete
상태의onClick
매개변수의 경우onStartClick
변수를 전달합니다.stringResourceId
매개변수의 경우 문자열 리소스 IDR.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)) }
}
}
}
...
앱 실행
- 앱을 실행하고 Start(시작)를 클릭합니다.
- 다양한 상태와 표시되는 UI가 어떻게 상응하는지 알아보려면 Background Task Inspector 창을 참고하세요.
SystemJobService
는 작업자 실행을 관리하는 구성요소입니다.
worker가 실행되는 동안 UI에 Cancel Work(작업 취소) 버튼과 원형 진행 상태 표시기가 표시됩니다.
worker가 완료되면 UI가 업데이트되어 Start(시작) 버튼이 예상대로 표시됩니다.
5. 최종 출력 표시
이 섹션에서는 표시할 준비가 된 블러 처리된 이미지가 있으면 항상 See File(파일 보기) 라벨의 버튼이 표시되도록 앱을 구성합니다.
See File(파일 보기) 버튼 만들기
See File(파일 보기) 버튼은 BlurUiState
가 Complete
일 때만 표시됩니다.
ui/BluromaticScreen.kt
파일을 열고BlurActions
컴포저블로 이동합니다.- Start(시작) 버튼과 See File(파일 보기) 버튼 사이에 공간을 추가하려면
BlurUiState.Complete
블록 내에Spacer
컴포저블을 추가합니다. - 새
FilledTonalButton
컴포저블을 추가합니다. onClick
매개변수의 경우onSeeFileClick(blurUiState.outputUri)
을 전달합니다.Button
의 콘텐츠 매개변수에Text
컴포저블을 추가합니다.Text
의text
매개변수의 경우 문자열 리소스 IDR.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
변수에 종속됩니다.
ui/BlurViewModel.kt
파일의map()
변환 내에서 새 변수outputImageUri
를 만듭니다.- 이 새로운 변수에
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 {
// ...
- 작업자가 완료되고 변수가 채워지면 표시할 블러 처리된 이미지가 있음을 나타냅니다.
outputImageUri.isNullOrEmpty()
를 호출하여 이 변수가 채워졌는지 확인할 수 있습니다.
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를 전달합니다. 도우미 함수는 인텐트를 만들고 이를 사용하여 저장된 이미지를 표시하는 새 활동을 시작합니다.
ui/BluromaticScreen.kt
파일을 엽니다.BluromaticScreenContent()
함수의 구성 가능한BlurActions()
함수 호출에서currentUri
라는 단일 매개변수를 사용하는onSeeFileClick
매개변수에 관한 람다 함수를 만들기 시작합니다. 이 방식은 저장된 이미지의 URI를 저장합니다.
ui/BluromaticScreen.kt
// ...
BlurActions(
blurUiState = blurUiState,
onStartClick = { applyBlur(selectedValue) },
onSeeFileClick = { currentUri ->
},
onCancelClick = { cancelWork() },
modifier = Modifier.fillMaxWidth()
)
// ...
- 람다 함수의 본문 내에서
showBlurredImage()
도우미 함수를 호출합니다. - 첫 번째 매개변수의 경우
context
변수를 전달합니다. - 두 번째 매개변수의 경우
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(파일 보기) 버튼이 표시됩니다. 이 버튼을 클릭하면 저장된 파일로 이동합니다.
6. 작업 취소
이전에 Cancel Work(작업 취소) 버튼을 추가했으므로 이제 코드를 추가하여 버튼의 작업을 실행할 수 있습니다. WorkManager를 사용하면 ID, 태그, 고유 체인 이름을 사용하여 작업을 취소할 수 있습니다.
이 경우 특정 단계뿐만 아니라 체인의 모든 작업을 취소하려고 하므로 고유 체인 이름으로 작업을 취소하는 것이 좋습니다.
이름으로 작업 취소
data/WorkManagerBluromaticRepository.kt
파일을 엽니다.cancelWork()
함수에서workManager.cancelUniqueWork()
함수를 호출합니다.- 고유 체인 이름
IMAGE_MANIPULATION_WORK_NAME
을 전달하여 호출이 해당 이름의 예약된 작업만 취소하도록 합니다.
data/WorkManagerBluromaticRepository.kt
override fun cancelWork() {
workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}
관심사 분리의 설계 원칙에 따라 구성 가능한 함수는 저장소와 직접 상호작용해서는 안 됩니다. 구성 가능한 함수는 ViewModel과 상호작용하고, ViewModel은 저장소와 상호작용합니다.
이 접근 방법은 좋은 설계 원칙입니다. 구성 가능한 함수가 직접 상호작용하지 않으므로 저장소를 변경해도 구성 가능한 함수를 변경할 필요가 없기 때문입니다.
ui/BlurViewModel.kt
파일을 엽니다.cancelWork()
라는 새 함수를 만들어 작업을 취소합니다.- 함수 내의
bluromaticRepository
객체에서cancelWork()
메서드를 호출합니다.
ui/BlurViewModel.kt
/**
* Call method from repository to cancel any ongoing WorkRequest
* */
fun cancelWork() {
bluromaticRepository.cancelWork()
}
Cancel Work 클릭 이벤트 설정
ui/BluromaticScreen.kt
파일을 엽니다.- 구성 가능한
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()
메서드가 실행되도록 하겠습니다.
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(작업 취소)를 클릭합니다. 전체 체인이 취소됩니다.
작업을 취소한 후에는 WorkInfo.State
가 CANCELLED
이므로 Start(시작) 버튼만 표시됩니다. 이러한 변경에 따라 blurUiState
변수가 BlurUiState.Default
로 설정됩니다. 그러면 UI가 초기 상태로 재설정되고 Start(시작) 버튼만 표시됩니다.
Background Task Inspector에 예상대로 Cancelled(취소됨) 상태가 표시됩니다.
7. 작업 제약 조건
마지막으로 WorkManager
는 Constraints
를 지원합니다. 제약 조건은 WorkRequest를 실행하기 전에 충족해야 하는 요구사항입니다.
제약 조건의 예로는 requiresDeviceIdle()
과 requiresStorageNotLow()
가 있습니다.
requiresDeviceIdle()
제약 조건의 경우true
값이 전달되면 기기가 유휴 상태일 때만 작업이 실행됩니다.requiresStorageNotLow()
제약 조건의 경우true
값이 전달되면 저장용량이 부족하지 않을 때만 작업이 실행됩니다.
Blur-O-Matic의 경우 blurWorker
작업 요청을 실행하기 전에 기기의 배터리 잔량 수준이 낮아서는 안 된다는 제약 조건을 추가합니다. 이 제약 조건을 사용하면 작업 요청이 지연되고 기기의 배터리가 부족하지 않은 경우에만 실행됩니다.
배터리 부족 상태 아님 조건 만들기
data/WorkManagerBluromaticRepository.kt
파일에서 다음 단계를 완료합니다.
applyBlur()
메서드로 이동합니다.continuation
변수를 선언하는 코드 뒤에constraints
라는 새 변수를 만듭니다. 이 변수는 생성되는 제약 조건의Constraints
객체를 포함합니다.Constraints.Builder()
함수를 호출하여 Constraints 객체용 빌더를 만들고 새 변수를 할당합니다.
data/WorkManagerBluromaticRepository.kt
import androidx.work.Constraints
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
// ...
setRequiresBatteryNotLow()
메서드를 호출에 체이닝하고true
값을 전달하여 기기의 배터리가 부족하지 않을 때만WorkRequest
가 실행되도록 합니다.
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
// ...
.build()
메서드 호출을 체이닝하여 객체를 빌드합니다.
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.build()
// ...
blurBuilder
작업 요청에 제약 조건 객체를 추가하려면.setConstraints()
메서드 호출을 체이닝하고 제약 조건 객체를 전달합니다.
data/WorkManagerBluromaticRepository.kt
// ...
blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))
blurBuilder.setConstraints(constraints) // Add this code
//...
에뮬레이터로 테스트
- 에뮬레이터에서 Extended Controls 창의 Charge level을 15% 이하로 변경하여 배터리 부족 시나리오를 시뮬레이션합니다. Charger connection은 AC charger로, Battery status는 Not charging으로 변경합니다.
- 앱을 실행하고 Start(시작)를 클릭하여 이미지 블러 처리를 시작합니다.
에뮬레이터의 배터리 잔량 수준이 낮음으로 설정되어 있으므로 WorkManager
는 제약 조건으로 인해 blurWorker
작업 요청을 실행하지 않습니다. 큐에 추가되지만 제약 조건이 충족될 때까지 지연됩니다. Background Task Inspector 탭에서 이 지연을 확인할 수 있습니다.
- 실행되지 않았음을 확인한 후 배터리 잔량 수준을 천천히 높입니다.
배터리 잔량 수준이 약 25%에 도달한 후에는 제약 조건이 충족되고 지연 작업이 실행됩니다. 이 결과는 Background Task Inspector 탭에 표시됩니다.
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 파일로 프로젝트를 동기화합니다.
테스트 클래스 만들기
- app > src 디렉터리에 UI 테스트용 디렉터리를 만듭니다.
androidTest/java
디렉터리에WorkerInstrumentationTest
라는 새 Kotlin 클래스를 만듭니다.
CleanupWorker
테스트 작성
단계에 따라, CleanupWorker
구현을 확인하는 테스트를 작성합니다. 안내에 따라 이 인증을 직접 구현해 보세요. 솔루션은 단계 마지막에 제공됩니다.
WorkerInstrumentationTest.kt
에서Context
인스턴스를 보유할lateinit
변수를 만듭니다.@Before
주석이 지정된setUp()
메서드를 만듭니다.setUp()
메서드에서ApplicationProvider
의 애플리케이션 컨텍스트로lateinit
컨텍스트 변수를 초기화합니다.cleanupWorker_doWork_resultSuccess()
라는 테스트 함수를 만듭니다.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
를 사용합니다. 코루틴의 스레딩 복잡성을 처리하기 때문입니다.
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 {
}
}
}
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()
에 도달하면 파일 디렉터리에 액세스하는 데 오류가 없습니다.
이제 작업자의 성공을 나타내는 어설션을 만들 차례입니다.
- 작업자의 결과가
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
구현을 확인하는 테스트를 작성합니다. 안내에 따라 이 인증을 직접 구현해 보세요. 솔루션은 단계 마지막에 제공됩니다.
WorkerInstrumentationTest.kt
에서blurWorker_doWork_resultSuccessReturnsUri()
라는 새 테스트 함수를 만듭니다.
BlurWorker
에는 처리할 이미지가 필요합니다. 따라서 BlurWorker
인스턴스를 빌드하려면 이러한 이미지가 포함된 입력 데이터가 필요합니다.
- 테스트 함수 외부에서 모의 URI 입력을 만듭니다. 모의 URI는 키와 URI 값을 포함하는 쌍입니다. 이 키-값 쌍에 다음 예시 코드를 사용합니다.
KEY_IMAGE_URI to "android.resource://com.example.bluromatic/drawable/android_cupcake"
blurWorker_doWork_resultSuccessReturnsUri()
함수 내에BlurWorker
를 빌드하고setInputData()
메서드를 통해 작업 데이터로 만든 모의 URI 입력을 전달해야 합니다.
CleanupWorker
테스트와 마찬가지로 runBlocking
내부의 작업자 구현을 호출해야 합니다.
runBlocking
블록을 만듭니다.runBlocking
블록 내에서doWork()
를 호출합니다.
CleanupWorker
와 달리 BlurWorker
에는 테스트용으로 사용 가능한 출력 데이터가 있습니다.
- 출력 데이터에 액세스하려면
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)
}
}
- 작업자의 성공을 나타내는 어설션을 만듭니다. 예를 들어
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
키가 포함되어 있음을 나타내는 어설션을 만듭니다.
- 출력 데이터에
"file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-"
문자열로 시작하는 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)
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
에서 SaveImageToFileWorker
를 BlurWorker
뒤에 연속으로 WorkManager에 추가합니다. 따라서 동일한 입력 데이터를 가집니다. 입력 데이터에서 URI를 가져와서 비트맵을 만든 다음 이 비트맵을 디스크에 파일로 씁니다. 작업이 성공하면 결과적으로 이미지 URL이 출력됩니다. SaveImageToFileWorker
테스트는 BlurWorker
테스트와 매우 비슷하며, 유일한 차이점은 출력 데이터입니다.
SaveImageToFileWorker
테스트를 직접 작성할 수 있는지 확인하세요. 완료되면 아래의 솔루션을 확인할 수 있습니다. BlurWorker
테스트에 사용한 접근 방식을 떠올려 보세요.
- 작업자를 빌드하고 입력 데이터를 전달합니다.
runBlocking
블록을 만듭니다.- 작업자에서
doWork()
를 호출합니다. - 결과가 성공적인지 확인합니다.
- 출력에 올바른 키와 값이 있는지 확인합니다.
솔루션은 다음과 같습니다.
@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 Inspector가 Blur-O-Matic의 작업자를 검사하기 위해 제공하는 몇 가지 기능을 알아봅니다.
- 기기나 에뮬레이터에서 Blur-O-Matic 앱을 실행합니다.
- View > Tool Windows > App Inspection(보기 > 도구 창 > 앱 검사)으로 이동합니다.
- Background Task Inspector 탭을 선택합니다.
- 필요한 경우 드롭다운 메뉴에서 기기와 실행 중인 프로세스를 선택합니다.
예시 이미지에서 프로세스는 com.example.bluromatic
입니다. 자동으로 프로세스가 선택될 수도 있습니다. 프로세스가 잘못 선택된 경우, 변경할 수 있습니다.
- Workers 드롭다운 메뉴를 클릭합니다. 현재 실행 중인 worker가 없습니다. 이미지를 블러 처리하려고 시도하지 않았기 때문입니다.
- 앱에서 More blurred(블러가 더 처리됨)를 선택하고 Start(시작)를 클릭합니다. Workers 드롭다운에 일부 콘텐츠가 즉시 표시됩니다.
이제 Workers 드롭다운에 다음과 같이 표시됩니다.
worker 테이블에는 worker의 이름, 서비스(이 경우 SystemJobService
), 각각의 상태, 타임스탬프가 표시됩니다. 이전 단계의 스크린샷에서 BlurWorker
와 CleanupWorker
가 작업을 성공적으로 완료했음을 알 수 있습니다.
검사기를 사용하여 작업을 취소할 수도 있습니다.
- 큐에 있는 작업자를 선택하고 툴바에서 Cancel Selected Worker(선택한 작업자 취소) 를 클릭합니다.
작업 세부정보 검사
- Workers 테이블에서 worker를 클릭합니다.
그러면 Task Details(작업 세부정보) 창이 표시됩니다.
- Task Details(작업 세부정보)에 표시된 정보를 검토합니다.
세부정보에 다음 카테고리가 표시됩니다.
- Description: 이 섹션에는 정규화된 패키지가 있는 작업자 클래스 이름과 할당된 태그, 이 작업자의 UUID가 나열됩니다.
- Execution: 이 섹션에는 작업자의 제약 조건(있는 경우)과 실행 빈도, 상태, 이 작업자를 만들고 대기열에 추가한 클래스가 표시됩니다. BlurWorker에는 배터리가 부족할 때 실행이 차단되는 제약 조건이 있습니다. 제약 조건이 있는 작업자를 검사하면 이 섹션에 표시됩니다.
- WorkContuniation: 이 섹션에는 이 작업자가 작업 체인에서 어디에 있는지 표시됩니다. 작업 체인에 있는 다른 작업자의 세부정보를 확인하려면 UUID를 클릭하세요.
- Results: 이 섹션에는 선택한 작업자의 시작 시간과 재시도 횟수, 출력 데이터가 표시됩니다.
그래프 뷰
Blur-O-Matic에서 작업자가 체이닝된다는 점을 떠올려 보세요. Background Task Inspector는 작업자 종속 항목을 시각적으로 나타내는 그래프 뷰를 제공합니다.
Background Task Inspector 창 모서리에 있는 두 버튼으로 Show Graph View(그래프 뷰 보기)와 Show List View(목록 뷰 보기) 간에 전환할 수 있습니다.
- Show Graph View(그래프 뷰 보기) 를 클릭합니다.
그래프 뷰는 Blur-O-Matic 앱에 구현된 worker 종속 항목을 정확하게 나타냅니다.
- Show List View(목록 뷰 보기) 를 클릭하여 그래프 뷰를 종료합니다.
추가 기능
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
를 테스트하는 방법- 수동으로 작업자를 검사하고 작업자의 기능을 확인하는 방법