1. 소개
Android에는 지연 가능한 백그라운드 작업을 위한 다양한 옵션이 있습니다. 이 Codelab에서는 WorkManager에 관해 알아봅니다. WorkManager는 유연하고 간편하며 하위 버전과 호환되는 라이브러리로, 지연 가능한 백그라운드 작업을 지원합니다. WorkManager는 Android에서 권장되는 작업 스케줄러로, 지연 가능한 작업을 실행하도록 보장합니다.
WorkManager란?
WorkManager는 상황별 실행과 보장된 실행을 조합하여 적용해야 하는 백그라운드 작업을 위한 아키텍처 구성요소로서 Android Jetpack의 일부입니다. 상황별 실행을 적용하면 WorkManager가 최대한 빨리 백그라운드 작업을 실행합니다. 보장된 실행을 적용하면 WorkManager가 사용자가 앱을 벗어난 경우를 비롯한 다양한 상황에서 로직을 처리하여 작업을 시작합니다.
WorkManager는 매우 유연한 라이브러리로, 이외에도 다음과 같은 다양한 이점이 있습니다.
- 비동기 일회성 작업과 주기적인 작업 모두 지원
- 네트워크 상태, 저장공간, 충전 상태와 같은 제약 조건 지원
- 동시 작업 실행을 포함한 복잡한 작업 요청 체이닝
- 한 작업 요청의 출력이 다음 작업 요청의 입력으로 사용됨
- 하위 버전인 API 수준 14와 호환성 처리(참고 확인)
- Google Play 서비스를 사용하거나 사용하지 않고 작업
- 시스템 상태 권장사항 준수
- UI에 작업 요청 상태를 쉽게 표시하는 LiveData 지원
WorkManager가 적합한 작업
WorkManager 라이브러리는 사용자가 특정 화면이나 앱에서 나가더라도 완료하는 것이 좋은 작업에 적합합니다.
WorkManager는 아래와 같은 작업에 사용하는 것이 적합합니다.
- 로그 업로드
- 이미지에 필터 적용 및 이미지 저장
- 주기적으로 로컬 데이터를 네트워크와 동기화
WorkManager는 보장된 실행을 제공하지만, 모든 작업에 보장된 실행이 필요하지는 않습니다. 따라서 기본 스레드에서 모든 작업을 실행하기 위한 포괄적인 기능은 아닙니다. 어떤 작업에 WorkManager를 사용할지 자세히 알아보려면 백그라운드 처리 가이드를 참고하세요.
빌드할 항목
요즘은 스마트폰이 사진을 정말 잘 찍습니다. 신비로운 대상을 사진가가 안정적으로 흐리게 처리한 사진으로 찍는 시대는 이제 지났습니다.
이 Codelab에서는 사진을 블러 처리하여 결과를 파일에 저장하는 앱인 Blur-O-Matic을 작업합니다. 네스 호의 괴물인지 evelopera 장난감 잠수함인지 궁금하게 만드는 사진을 Blur-O-Matic을 통해 만들 수 있습니다.
학습할 내용
- 프로젝트에 WorkManager 추가
- 단순한 작업 예약
- 입력 및 출력 매개변수
- 작업 체이닝
- 고유 작업
- UI에 작업 상태 표시
- 작업 취소
- 작업 제약 조건
필요한 항목
- Android 스튜디오 최신 공개 버전이 있어야 합니다.
LiveData
및ViewModel
을 잘 알고 있어야 합니다. 이러한 클래스를 처음 사용하는 경우 Android 수명 주기 인식 구성요소 Codelab(ViewModel 및 LiveData 전용) 또는 뷰를 사용한 Room Codelab(아키텍처 구성요소 소개)을 참고하세요.
2. 설정
1단계 - 코드 다운로드
다음 링크를 클릭하면 이 Codelab의 모든 코드를 다운로드할 수 있습니다.
또는 원한다면 GitHub에서 WorkManager Codelab을 클론할 수도 있습니다.
$ git clone -b start_kotlin https://github.com/googlecodelabs/android-workmanager
2단계 - 앱 실행
앱을 실행합니다. 다음과 같은 화면이 표시됩니다.
화면에는 이미지를 얼마나 블러 처리할지 선택할 수 있는 라디오 버튼이 있습니다. 적용 버튼을 누르면 최종적으로 이미지가 블러 처리되어 저장됩니다.
지금은 앱이 블러를 적용하지 않습니다.
시작 코드에는 다음이 포함됩니다.
WorkerUtils
: 이 클래스에는 실제로 이미지를 블러 처리하는 코드와 나중에Notifications
를 표시하고, 비트맵을 파일에 저장하고, 앱 속도를 느리게 하는 데 사용하는 몇 가지 편의 메서드가 있습니다.BlurActivity
:* 이미지를 표시하고 흐림 수준을 선택하는 라디오 버튼이 포함된 활동입니다.BlurViewModel
:* 이 뷰 모델은BlurActivity
를 표시하는 데 필요한 데이터를 모두 저장합니다. WorkManager를 사용하여 백그라운드 작업을 시작하는 클래스이기도 합니다.Constants
: Codelab에서 사용할 상수가 포함된 정적 클래스입니다.res/activity_blur.xml
:BlurActivity
의 레이아웃 파일입니다.
***** 이 파일에만 코드를 작성합니다.
3. 앱에 WorkManager 추가
WorkManager
에는 아래의 Gradle 종속 항목이 필요합니다. 빌드 파일에 이미 포함되어 있는 항목입니다.
app/build.gradle
dependencies {
// WorkManager dependency
implementation "androidx.work:work-runtime-ktx:$versions.work"
}
여기에서 최신 버전의 안정적인 work-runtime-ktx
를 가져와 올바른 버전을 입력하세요. 현재 최신 버전은 다음과 같습니다.
build.gradle
versions.work = "2.7.1"
버전을 최신 버전으로 업데이트하는 경우 지금 동기화를 통해 프로젝트를 변경된 Gradle 파일과 동기화해야 합니다.
4. 첫 번째 WorkRequest 만들기
이 단계에서는 res/drawable
폴더의 android_cupcake.png
라는 이미지에 몇 가지 함수를 백그라운드에서 실행합니다. 이러한 함수는 이미지를 블러 처리한 후 임시 파일에 저장합니다.
WorkManager 기본사항
알아야 할 몇 가지 WorkManager 클래스가 있습니다.
Worker
: 백그라운드에서 실행하고자 하는 실제 작업의 코드를 여기에 입력합니다. 이 클래스를 확장하고doWork()
메서드를 재정의합니다.WorkRequest
: 작업 실행 요청을 나타냅니다.WorkRequest
를 만드는 과정에서Worker
를 전달합니다.WorkRequest
를 만들 때Worker
를 실행할 시점에 적용되는Constraints
등을 지정할 수도 있습니다.WorkManager
: 이 클래스는 실제로WorkRequest
를 예약하고 실행합니다. 지정된 제약 조건을 준수하면서 시스템 리소스에 부하를 분산하는 방식으로WorkRequest
를 예약합니다.
여기에서는 이미지를 블러 처리하는 코드를 포함하는 새 BlurWorker
를 정의합니다. 적용 버튼을 클릭하면 WorkRequest
가 생성된 다음 WorkManager
에 의해 큐에 추가됩니다.
1단계 - BlurWorker 만들기
workers
패키지에서 BlurWorker
라는 새 Kotlin 클래스를 만듭니다.
2단계 - 생성자 추가
Worker
에 BlurWorker
클래스의 종속 항목을 추가합니다.
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
}
3단계 - doWork() 재정의 및 구현
Worker
가 표시된 컵케이크 이미지를 블러 처리합니다.
작업이 언제 실행되는지 더 잘 확인하기 위해 WorkerUtil의 makeStatusNotification()
을 사용합니다. 이 메서드를 사용하면 화면 상단에 알림 배너를 쉽게 표시할 수 있습니다.
doWork()
메서드를 재정의하고 다음을 구현합니다. 섹션 끝에서 완성된 코드를 참고할 수 있습니다.
applicationContext
속성을 호출하여Context
를 가져옵니다. 이를appContext
이라는 새val
에 할당합니다. 처리할 다양한 비트맵 조작을 위해 필요합니다.- 사용자에게 이미지 블러 처리에 관해 알리는
makeStatusNotification
함수를 사용하여 상태 알림을 표시합니다. - 컵케이크 이미지에서
Bitmap
을 만듭니다.
val picture = BitmapFactory.decodeResource(
appContext.resources,
R.drawable.android_cupcake)
WorkerUtils
에서blurBitmap
메서드를 호출하여 블러 처리된 버전의 비트맵을 가져옵니다.WorkerUtils
에서writeBitmapToFile
메서드를 호출하여 이 비트맵을 임시 파일에 씁니다. 반환된 URI를 로컬 변수에 저장해야 합니다.WorkerUtils
에서makeStatusNotification
메서드를 호출하여 URI를 표시하는 알림을 만듭니다.Result.success()
를 반환합니다.- 3~6단계의 코드를 try/catch 문으로 래핑합니다. 일반
Throwable
을 포착합니다. - catch 문에서 로그 구문
Log.e(TAG, "Error applying blur")
를 사용하여 오류 메시지를 출력합니다. - catch 문에서
Result.failure()
를 반환합니다.
이 단계에서 완성된 코드는 아래와 같습니다.
**BlurWorker.**kt
package com.example.background.workers
import android.content.Context
import android.graphics.BitmapFactory
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.R
private const val TAG = "BlurWorker"
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
val appContext = applicationContext
makeStatusNotification("Blurring image", appContext)
return try {
val picture = BitmapFactory.decodeResource(
appContext.resources,
R.drawable.android_cupcake)
val output = blurBitmap(picture, appContext)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(appContext, output)
makeStatusNotification("Output is $outputUri", appContext)
Result.success()
} catch (throwable: Throwable) {
Log.e(TAG, "Error applying blur")
Result.failure()
}
}
}
4단계 - ViewModel에서 WorkManager 가져오기
ViewModel
에서 WorkManager
인스턴스의 클래스 변수를 만듭니다.
BlurViewModel.kt
private val workManager = WorkManager.getInstance(application)
5단계 - WorkManager에서 WorkRequest를 큐에 추가
이제 WorkRequest
를 만들고 WorkManager에 실행하도록 지시합니다. 두 가지 WorkRequest
유형이 있습니다.
OneTimeWorkRequest
: 한 번만 실행할WorkRequest
입니다.PeriodicWorkRequest
: 일정 주기로 반복할WorkRequest
입니다.
적용 버튼을 클릭할 때 한 번만 이미지를 블러 처리하도록 설정하고 싶습니다. 적용 버튼을 클릭하면 applyBlur
메서드가 호출되므로 이 메서드의 BlurWorker
에서 OneTimeWorkRequest
를 만듭니다. 그런 다음 WorkManager
인스턴스를 사용하여 WorkRequest.
를 큐에 추가합니다.
BlurViewModel's
applyBlur()
메서드에 다음 코드 줄을 추가합니다.
BlurViewModel.kt
internal fun applyBlur(blurLevel: Int) {
workManager.enqueue(OneTimeWorkRequest.from(BlurWorker::class.java))
}
6단계 - 코드 실행
코드를 실행합니다. 컴파일되고 진행 버튼을 누르면 알림이 표시됩니다. 더 흐리게 처리된 결과를 보기 위해 '더 흐리게 처리' 또는 '가장 흐리게 처리' 옵션을 선택해야 합니다.
이미지가 성공적으로 블러 처리되었는지 확인하려면 Android 스튜디오에서 Device File Explorer를 엽니다.
그런 다음 data > data > com.example.background > files > blur_filter_outputs> <URI>로 이동하여 컵케이크가 실제로 블러 처리되었는지 확인합니다.
5. 입력 및 출력 추가
리소스 디렉터리에서 이미지 애셋 블러 처리가 잘 구현되었습니다. 그러나 Blur-O-Matic이 추구하는 진정으로 획기적인 이미지 수정 앱이 되도록 하려면 사용자가 화면에 표시되는 이미지를 블러 처리할 수 있게 한 후, 사용자에게 블러 처리된 결과를 보여줄 수 있어야 합니다.
그러기 위해서 표시된 WorkRequest
의 입력으로 표시된 컵케이크 이미지의 URI를 제공한 다음 WorkRequest의 출력을 사용하여 최종 블러 처리된 이미지를 표시합니다.
1단계 - 데이터 입력 객체 만들기
입력 및 출력은 Data
객체를 통해 안팎으로 전달됩니다. Data
객체는 키-값 쌍의 경량 컨테이너입니다. WorkRequest
의 안팎으로 전달될 수 있는 소량의 데이터를 저장하기 위한 것입니다.
사용자 이미지의 URI를 번들로 전달할 것입니다. 이 URI는 imageUri
라는 변수에 저장됩니다.
BlurViewModel
에서 createInputDataForUri
라는 비공개 메서드를 만듭니다. 이 메서드는 다음과 같은 역할을 합니다.
Data.Builder
객체를 만듭니다. 요청이 있는 경우androidx.work.Data
를 가져옵니다.imageUri
가 null이 아닌URI
이면putString
메서드를 사용하여Data
객체에 추가합니다. 이 메서드는 키와 값을 사용합니다.Constants
클래스의 문자열 상수KEY_IMAGE_URI
를 사용할 수 있습니다.Data.Builder
객체에서build()
를 호출하여Data
객체를 만들고 반환합니다.
다음은 완료된 createInputDataForUri
메서드입니다.
BlurViewModel.kt
/**
* Creates the input data bundle which includes the Uri to operate on
* @return Data which contains the Image Uri as a String
*/
private fun createInputDataForUri(): Data {
val builder = Data.Builder()
imageUri?.let {
builder.putString(KEY_IMAGE_URI, imageUri.toString())
}
return builder.build()
}
2단계 - WorkRequest에 데이터 객체 전달
다음을 수행하도록 BlurViewModel
의 applyBlur
메서드를 변경합니다.
- 새
OneTimeWorkRequestBuilder
를 만듭니다. setInputData
를 호출하여createInputDataForUri
의 결과를 전달합니다.OneTimeWorkRequest
를 빌드합니다.- 작업 실행이 예약되도록
WorkManager
요청을 사용하여 작업 요청을 큐에 추가합니다.
다음은 완료된 applyBlur
메서드입니다.
BlurViewModel.kt
internal fun applyBlur(blurLevel: Int) {
val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
.setInputData(createInputDataForUri())
.build()
workManager.enqueue(blurRequest)
}
3단계 - BlurWorker의 doWork()를 업데이트하여 입력 가져오기
이제 전달한 URI를 Data
객체에서 가져오도록 BlurWorker
의 doWork()
메서드를 업데이트하겠습니다.
BlurWorker.kt
override fun doWork(): Result {
val appContext = applicationContext
// ADD THIS LINE
val resourceUri = inputData.getString(KEY_IMAGE_URI)
// ... rest of doWork()
}
4단계 - 지정된 URI 블러 처리
이제 URI를 사용하여 컵케이크 이미지를 블러 처리합니다.
- 이미지 리소스를 가져온 이전 코드를 삭제합니다.
val picture = BitmapFactory.decodeResource(appContext.
resources
, R.drawable.
android_cupcake
)
- 전달된
Data
에서 얻은resourceUri
가 비어 있지 않은지 확인합니다. picture
변수를 다음과 같이 전달된 이미지로 할당합니다.
val picture = BitmapFactory.decodeStream(
appContext.
contentResolver
.
`openInputStream(Uri.parse(resourceUri)))`
BlurWorker.kt
override fun doWork(): Result {
val appContext = applicationContext
val resourceUri = inputData.getString(KEY_IMAGE_URI)
makeStatusNotification("Blurring image", appContext)
return try {
// REMOVE THIS
// val picture = BitmapFactory.decodeResource(
// appContext.resources,
// R.drawable.android_cupcake)
if (TextUtils.isEmpty(resourceUri)) {
Log.e(TAG, "Invalid input uri")
throw IllegalArgumentException("Invalid input uri")
}
val resolver = appContext.contentResolver
val picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)))
val output = blurBitmap(picture, appContext)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(appContext, output)
Result.success()
} catch (throwable: Throwable) {
Log.e(TAG, "Error applying blur")
throwable.printStackTrace()
Result.failure()
}
}
5단계 - 임시 URI 출력
이제 이 Worker를 완료했으며 Result.success()
에서 출력 URI를 반환할 수 있습니다. 추가 작업을 위해 이 임시 이미지에 다른 작업자가 쉽게 액세스할 수 있도록 출력 URI를 출력 데이터로 제공합니다. 이렇게 하면 다음 장에서 작업자 체인을 만들 때 유용합니다. 방법은 다음과 같습니다.
- 새
Data
를 만들고 입력의 경우와 마찬가지로outputUri
를String
으로 저장합니다. 같은 키(KEY_IMAGE_URI
)를 사용합니다. Result.success(Data outputData)
메서드를 사용하여 WorkManager에 반환합니다.
BlurWorker.kt
doWork()
에서 Result.success()
줄을 다음으로 수정합니다.
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
Result.success(outputData)
6단계 - 앱 실행
이 시점에서 앱을 실행해야 합니다. 컴파일되고 Device File Explorer를 통해 블러 처리된 이미지를 볼 때와 똑같이 동작해야 하지만 아직 화면에서는 실행되지 않습니다.
블러 처리된 다른 이미지를 확인하려면 Android 스튜디오에서 Device File Explorer를 열고 지난 단계에서처럼 data/data/com.example.background/files/blur_filter_outputs/<URI>로 이동합니다.
이미지를 보려면 동기화가 필요할 수도 있습니다.
수고하셨습니다. WorkManager
를 사용하여 입력 이미지를 블러 처리했습니다.
6. 작업 체이닝
지금은 단일 작업, 즉 이미지 블러 처리만 하고 있습니다. 훌륭한 첫 단계이지만 일부 핵심 기능이 다음과 같이 누락되었습니다.
- 임시 파일을 정리하지 않음
- 이미지를 실제로 영구 파일에 저장하지 않음
- 사진의 블러 처리 양이 항상 같음
WorkManager 작업 체인을 사용하여 위의 기능을 추가합니다.
WorkManager를 사용하면 순서대로 실행되거나 동시에 실행되는 별도의 WorkerRequest
를 만들 수 있습니다. 이 단계에서는 다음과 같은 작업 체인을 만듭니다.
WorkRequest
는 상자로 표시되어 있습니다.
체이닝을 위한 또 다른 멋진 기능은 한 WorkRequest
의 출력이 체인 내 다음 WorkRequest
의 입력이 된다는 점입니다. 각 WorkRequest
간에 전달되는 입력과 출력은 파란색 텍스트로 표시되어 있습니다.
1단계 - 정리 Worker와 저장 Worker 만들기
먼저 필요한 Worker
클래스를 모두 정의합니다. 이미지를 블러 처리하는 Worker
는 이미 있지만 임시 파일을 정리하는 Worker
와 이미지를 영구적으로 저장하는 Worker
도 필요합니다.
workers
패키지에 Worker
를 확장하는 새 클래스 두 개를 만듭니다.
하나는 CleanupWorker
로, 다른 하나는 SaveImageToFileWorker
로 지정해야 합니다.
2단계 - Worker 확장 지정
Worker
클래스에서 CleanupWorker
클래스를 확장합니다. 필수 생성자 매개변수를 추가합니다.
class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
}
3단계 - doWork()를 재정의하여 CleanupWorker용으로 구현
CleanupWorker
는 입력을 받거나 출력을 전달할 필요가 없습니다. 임시 파일이 있으면 항상 삭제합니다. 이 Codelab에서 파일 조작은 범위를 벗어나므로 아래의 CleanupWorker
코드를 복사하면 됩니다.
CleanupWorker.kt
package com.example.background.workers
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File
/**
* Cleans up temporary files generated during blurring process
*/
private const val TAG = "CleanupWorker"
class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
// Makes a notification when the work starts and slows down the work so that
// it's easier to see each WorkRequest start, even on emulated devices
makeStatusNotification("Cleaning up old temporary files", applicationContext)
sleep()
return 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) {
exception.printStackTrace()
Result.failure()
}
}
}
4단계 - doWork()를 재정의하여 SaveImageToFileWorker용으로 구현
SaveImageToFileWorker
는 입력과 출력을 처리합니다. 입력은 KEY_IMAGE_URI
키로 저장된 임시 블러 처리된 이미지 URI의 String
입니다. 또한 키가 KEY_IMAGE_URI
로 저장된 블러 처리된 이미지의 URI인 String
도 출력됩니다.
이 Codelab은 파일 조작에 관해 다루지 않으므로, 아래의 코드를 사용하세요. KEY_IMAGE_URI
키로 resourceUri
및 output
값이 어떻게 검색되는지 확인하세요. 입력 및 출력에 관한 최근 단계에서 작성한 코드와 매우 유사합니다(동일한 키를 모두 사용함).
SaveImageToFileWorker.kt
package com.example.background.workers
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.workDataOf
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.KEY_IMAGE_URI
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Saves the image to a permanent file
*/
private const val TAG = "SaveImageToFileWorker"
class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
private val title = "Blurred Image"
private val dateFormatter = SimpleDateFormat(
"yyyy.MM.dd 'at' HH:mm:ss z",
Locale.getDefault()
)
override fun doWork(): Result {
// Makes a notification when the work starts and slows down the work so that
// it's easier to see each WorkRequest start, even on emulated devices
makeStatusNotification("Saving image", applicationContext)
sleep()
val resolver = applicationContext.contentResolver
return try {
val resourceUri = inputData.getString(KEY_IMAGE_URI)
val bitmap = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)))
val imageUrl = MediaStore.Images.Media.insertImage(
resolver, bitmap, title, dateFormatter.format(Date()))
if (!imageUrl.isNullOrEmpty()) {
val output = workDataOf(KEY_IMAGE_URI to imageUrl)
Result.success(output)
} else {
Log.e(TAG, "Writing to MediaStore failed")
Result.failure()
}
} catch (exception: Exception) {
exception.printStackTrace()
Result.failure()
}
}
}
5단계 - BlurWorker 알림 수정
올바른 폴더에 이미지를 저장하는 Worker
체인을 만들었으므로 이제 에뮬레이션된 기기에서도 WorkerUtils
에서 정의된 sleep()
메서드를 사용하여 각 WorkRequest
의 시작을 더 쉽게 볼 수 있도록 작업 속도를 늦출 수 있습니다. BlurWorker
의 최종 버전은 다음과 같습니다.
BlurWorker.kt
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
val appContext = applicationContext
val resourceUri = inputData.getString(KEY_IMAGE_URI)
makeStatusNotification("Blurring image", appContext)
// ADD THIS TO SLOW DOWN THE WORKER
sleep()
// ^^^^
return try {
if (TextUtils.isEmpty(resourceUri)) {
Timber.e("Invalid input uri")
throw IllegalArgumentException("Invalid input uri")
}
val resolver = appContext.contentResolver
val picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)))
val output = blurBitmap(picture, appContext)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(appContext, output)
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
Result.success(outputData)
} catch (throwable: Throwable) {
throwable.printStackTrace()
Result.failure()
}
}
6단계 - WorkRequest 체인 만들기
하나만 실행하는 것이 아니라 WorkRequest
체인을 실행하도록 BlurViewModel
의 applyBlur
메서드를 수정해야 합니다. 현재 코드는 다음과 같습니다.
BlurViewModel.kt
val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
.setInputData(createInputDataForUri())
.build()
workManager.enqueue(blurRequest)
workManager.enqueue()
를 호출하는 대신 workManager.beginWith()
를 호출합니다. 그러면 WorkRequest
체인을 정의하는 WorkContinuation
이 반환됩니다. then()
메서드를 호출하여 이 작업 요청 체인에 추가할 수 있습니다. 예를 들어 WorkRequest
객체 세 개(workA
, workB
, workC
)가 있는 경우 다음과 같이 합니다.
// Example code, don't copy to the project
val continuation = workManager.beginWith(workA)
continuation.then(workB) // FYI, then() returns a new WorkContinuation instance
.then(workC)
.enqueue() // Enqueues the WorkContinuation which is a chain of work
그러면 다음과 같은 WorkRequest 체인이 생성되고 실행됩니다.
applyBlur
에서 CleanupWorker
WorkRequest
, BlurImage
WorkRequest
, SaveImageToFile
WorkRequest
의 체인을 만듭니다. BlurImage
WorkRequest
에 입력을 전달합니다.
이를 위한 코드는 다음과 같습니다.
BlurViewModel.kt
internal fun applyBlur(blurLevel: Int) {
// Add WorkRequest to Cleanup temporary images
var continuation = workManager
.beginWith(OneTimeWorkRequest
.from(CleanupWorker::class.java))
// Add WorkRequest to blur the image
val blurRequest = OneTimeWorkRequest.Builder(BlurWorker::class.java)
.setInputData(createInputDataForUri())
.build()
continuation = continuation.then(blurRequest)
// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequest.Builder(SaveImageToFileWorker::class.java).build()
continuation = continuation.then(save)
// Actually start the work
continuation.enqueue()
}
그러면 컴파일되고 실행됩니다. 이제 진행 버튼을 누르고 여러 작업자가 실행 중일 때 알림을 확인할 수 있습니다. 계속해서 Device File Explorer에서 블러 처리된 이미지를 확인할 수 있으며, 향후에 사용자가 기기에서 블러 처리된 이미지를 확인할 수 있도록 버튼을 추가할 예정입니다.
아래 스크린샷에서는 현재 실행 중인 작업자가 알림 메시지에 표시되는 것을 볼 수 있습니다.
7단계 - BlurWorker 반복
서로 다른 양으로 이미지를 블러 처리하는 기능을 추가합니다. applyBlur
에 전달된 blurLevel
매개변수를 사용하여 매개변수에 지정된 수량의 WorkRequest
작업을 체인에 추가합니다. 첫 번째 WorkRequest
만 URI가 필요하며 URI 입력을 받아야 합니다.
직접 해 보고 아래의 코드와 비교하세요.
BlurViewModel.kt
internal fun applyBlur(blurLevel: Int) {
// Add WorkRequest to Cleanup temporary images
var continuation = workManager
.beginWith(OneTimeWorkRequest
.from(CleanupWorker::class.java))
// Add WorkRequests to blur the image the number of times requested
for (i in 0 until blurLevel) {
val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
// Input the Uri if this is the first blur operation
// After the first blur operation the input will be the output of previous
// blur operations.
if (i == 0) {
blurBuilder.setInputData(createInputDataForUri())
}
continuation = continuation.then(blurBuilder.build())
}
// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.build()
continuation = continuation.then(save)
// Actually start the work
continuation.enqueue()
}
Device File Explorer를 열어 블러 처리된 이미지를 확인합니다. 출력 폴더에는 블러 처리의 중간 단계 이미지, 선택한 블러 수준에 따라 블러 처리된 이미지를 표시하는 최종 이미지 등 블러 처리된 이미지가 여러 개 있습니다.
잘하셨습니다. 이제 원하는 양만큼 이미지를 블러 처리할 수 있습니다. 정말 유용하죠.
7. 고유 작업 보장
체인을 사용해 봤습니다. 이제 WorkManager의 또 다른 강력한 기능인 고유 작업 체인을 알아보겠습니다.
작업 체인을 한 번에 하나씩만 실행해야 하는 경우가 있습니다. 예를 들어 로컬 데이터를 서버와 동기화하는 작업 체인이 있는 경우, 첫 번째 데이터 동기화가 완료된 후에 새 동기화가 시작되도록 할 수 있습니다. 이렇게 하려면 beginWith
대신 beginUniqueWork
를 사용하고 고유한 String
이름을 제공합니다. 함께 참조하고 쿼리할 수 있도록 전체 작업 요청 체인을 지정합니다.
파일을 블러 처리하는 작업 체인이 고유하도록 beginUniqueWork
를 사용합니다. 키로 IMAGE_MANIPULATION_WORK_NAME
을 전달합니다. ExistingWorkPolicy
도 전달해야 합니다. 사용할 수 있는 옵션은 REPLACE
, KEEP
, APPEND
입니다.
사용자가 현재 이미지가 완료되기 전에 다른 이미지를 블러 처리하려는 경우 현재 이미지가 중지되고 새 이미지가 블러 처리되도록 지정할 계획이므로 REPLACE
를 사용합니다.
고유 작업 연속 처리를 시작하는 코드는 다음과 같습니다.
BlurViewModel.kt
// 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은 한 번에 사진 한 장만 블러 처리합니다.
8. 태그 지정 및 작업 상태 표시
이 섹션에서는 LiveData를 많이 사용하므로, 섹션 내용을 제대로 이해하려면 LiveData를 숙지해야 합니다. LiveData는 observable 클래스로, 수명 주기 인식 데이터 홀더입니다.
LiveData 또는 observable을 처음 작업하는 경우에는 문서나 Android 수명 주기 인식 구성요소 Codelab을 확인하세요.
다음으로 크게 변경할 것은 Work의 실행에 따라 앱에 표시되는 내용을 실제로 바꾸는 것입니다.
WorkInfo
객체가 포함된 LiveData
를 가져와서 WorkRequest
의 상태를 가져올 수 있습니다. WorkInfo
는 WorkRequest
의 현재 상태에 관한 다음과 같은 세부정보가 포함된 객체입니다.
- 작업의 상태(
BLOCKED
,CANCELLED
,ENQUEUED
,FAILED
,RUNNING
,SUCCEEDED
중에서) WorkRequest
가 완료된 경우 작업의 모든 출력 데이터
다음 표에서는 LiveData<WorkInfo>
객체나 LiveData<List<WorkInfo>>
객체를 가져오는 세 가지 방법과 각 결과를 설명합니다.
유형 | WorkManager 메서드 | 설명 |
ID를 사용하여 작업 가져오기 |
| 각 |
고유 체인 이름을 사용하여 작업 가져오기 |
| 방금 본 것처럼 |
태그를 사용하여 작업 가져오기 |
| 마지막으로, 선택적으로 WorkRequest를 String으로 태그 지정할 수 있습니다. 동일한 태그를 사용하여 여러 |
SaveImageToFileWorker
WorkRequest
를 태그 지정하면 getWorkInfosByTag
를 사용하여 가져올 수 있습니다. WorkManager ID를 사용하는 대신 태그를 사용하여 작업의 라벨을 지정하겠습니다. 왜냐하면 사용자가 여러 이미지를 블러 처리하는 경우 모든 이미지 저장 WorkRequest
의 태그가 같지만 ID는 같지 않기 때문입니다. 또한 태그를 선택할 수도 있습니다.
getWorkInfosForUniqueWork
를 사용하지 않습니다. 모든 블러 WorkRequest
및 정리 WorkRequest
의 WorkInfo
도 반환하기 때문입니다(이렇게 반환하려면 이미지 저장 WorkRequest
를 찾기 위한 추가 로직이 필요함).
1단계 - 작업 태그 지정
applyBlur
에서 SaveImageToFileWorker
를 만들 때 String
상수 TAG_OUTPUT
을 사용하여 작업에 태그를 지정합니다.
BlurViewModel.kt
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.addTag(TAG_OUTPUT) // <-- ADD THIS
.build()
2단계 - WorkInfo 가져오기
작업에 태그를 지정했으므로 이제 WorkInfo
를 가져올 수 있습니다.
BlurViewModel
에서outputWorkInfos
라는 새 클래스 변수LiveData<List<WorkInfo>>
를 선언합니다.BlurViewModel
에서WorkManager.getWorkInfosByTagLiveData
를 사용하여WorkInfo
를 가져오는 init 블록을 추가합니다.
필요한 코드는 다음과 같습니다.
BlurViewModel.kt
// New instance variable for the WorkInfo
internal val outputWorkInfos: LiveData<List<WorkInfo>>
// Modify the existing init block in the BlurViewModel class to this:
init {
imageUri = getImageUri(application.applicationContext)
// This transformation makes sure that whenever the current work Id changes the WorkInfo
// the UI is listening to changes
outputWorkInfos = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
}
3단계 - WorkInfo 표시
WorkInfo
의 LiveData
를 가져왔으며 이제 BlurActivity
에서 관찰할 수 있습니다. 관찰자에서 다음을 따릅니다.
WorkInfo
목록이 null이 아닌지, 그리고 목록에WorkInfo
객체가 있는지 확인합니다. 없으면 적용 버튼을 아직 클릭하지 않은 것이므로 돌아갑니다.- 목록의 첫 번째
WorkInfo
를 가져옵니다. 작업 체인을 고유하게 만들었으므로TAG_OUTPUT
으로 태그 지정된WorkInfo
는 하나만 있습니다. workInfo.state.isFinished
를 사용하여 작업이 완료 상태인지 확인합니다.- 완료되지 않은 경우
showWorkInProgress()
을 호출하여 진행 버튼을 숨기고 작업 취소 버튼과 진행률 표시줄을 표시합니다. - 작업이 완료되면
showWorkFinished()
를 호출하여 작업 취소 버튼 및 진행률 표시줄을 숨기고 진행 버튼을 표시합니다.
코드는 다음과 같습니다.
참고: 요청이 있는 경우 androidx.lifecycle.Observer
를 가져옵니다.
BlurActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
...
// Observe work status, added in onCreate()
viewModel.outputWorkInfos.observe(this, workInfosObserver())
}
// Define the observer function
private fun workInfosObserver(): Observer<List<WorkInfo>> {
return Observer { listOfWorkInfo ->
// Note that these next few lines grab a single WorkInfo if it exists
// This code could be in a Transformation in the ViewModel; they are included here
// so that the entire process of displaying a WorkInfo is in one location.
// If there are no matching work info, do nothing
if (listOfWorkInfo.isNullOrEmpty()) {
return@Observer
}
// We only care about the one output status.
// Every continuation has only one worker tagged TAG_OUTPUT
val workInfo = listOfWorkInfo[0]
if (workInfo.state.isFinished) {
showWorkFinished()
} else {
showWorkInProgress()
}
}
}
4단계 - 앱 실행
앱을 실행합니다. 앱이 컴파일되고 실행되지만 이번에는 작동할 때 진행률 표시줄이 표시되고 취소 버튼도 표시됩니다.
9. 최종 출력 표시
각 WorkInfo
에는 저장된 최종 이미지가 있는 출력 Data
객체를 가져올 수 있는 getOutputData
메서드도 있습니다. Kotlin에서는 언어가 자동으로 생성하는 변수, outputData
를 사용하여 이 메서드에 액세스할 수 있습니다. 표시할 준비가 된 블러 처리된 이미지가 있으면 항상 파일 보기라는 버튼을 표시해 보겠습니다.
1단계 - '파일 보기' 버튼 만들기
activity_blur.xml
레이아웃에 숨겨진 버튼이 이미 있습니다. BlurActivity
에 있는 outputButton
입니다.
onCreate()
의 BlurActivity
에서 버튼에 클릭 리스너를 설정합니다. 리스너는 URI를 가져와서 이 URI를 보는 활동을 열어야 합니다. 다음 코드를 사용할 수 있습니다.
BlurActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
// Setup view output image file button
binding.seeFileButton.setOnClickListener {
viewModel.outputUri?.let { currentUri ->
val actionView = Intent(Intent.ACTION_VIEW, currentUri)
actionView.resolveActivity(packageManager)?.run {
startActivity(actionView)
}
}
}
}
2단계 - URI 설정 및 버튼 표시
작동하기 위해 WorkInfo
관찰자에 적용해야 하는 몇 가지 최종 조정이 있습니다.
WorkInfo
가 완료된 경우workInfo.outputData
를 사용하여 출력 데이터를 가져옵니다.- 그런 다음 출력 URI를 가져옵니다.
Constants.KEY_IMAGE_URI
키를 사용해 저장된다는 점을 기억하세요. - URI가 비어 있지 않으면 올바르게 저장된 것입니다.
outputButton
을 표시하고 URI를 사용하여 뷰 모델에서setOutputUri
를 호출합니다.
BlurActivity.kt
private fun workInfosObserver(): Observer<List<WorkInfo>> {
return Observer { listOfWorkInfo ->
// Note that these next few lines grab a single WorkInfo if it exists
// This code could be in a Transformation in the ViewModel; they are included here
// so that the entire process of displaying a WorkInfo is in one location.
// If there are no matching work info, do nothing
if (listOfWorkInfo.isNullOrEmpty()) {
return@Observer
}
// We only care about the one output status.
// Every continuation has only one worker tagged TAG_OUTPUT
val workInfo = listOfWorkInfo[0]
if (workInfo.state.isFinished) {
showWorkFinished()
// Normally this processing, which is not directly related to drawing views on
// screen would be in the ViewModel. For simplicity we are keeping it here.
val outputImageUri = workInfo.outputData.getString(KEY_IMAGE_URI)
// If there is an output file show "See File" button
if (!outputImageUri.isNullOrEmpty()) {
viewModel.setOutputUri(outputImageUri)
binding.seeFileButton.visibility = View.VISIBLE
}
} else {
showWorkInProgress()
}
}
}
3단계 - 코드 실행
코드를 실행합니다. 클릭 가능한 새 파일 보기 버튼이 표시됩니다. 이 버튼을 클릭하면 출력 파일로 이동합니다.
10. 작업 취소
Cancel Work 버튼을 추가했으므로 이 버튼이 동작하도록 코드를 추가합니다. WorkManager를 사용하면 ID, 태그, 고유 체인 이름을 사용하여 작업을 취소할 수 있습니다.
여기서는 특정 단계뿐 아니라 체인의 모든 작업을 취소하려고 하므로 고유한 체인 이름으로 작업을 취소하는 것이 좋습니다.
1단계 - 이름으로 작업 취소
BlurViewModel
에서 cancelWork()
라는 새 메서드를 추가하여 고유 작업을 취소합니다. workManager
의 함수 호출 cancelUniqueWork
내에서 IMAGE_MANIPULATION_WORK_NAME
태그를 전달합니다.
BlurViewModel.kt
internal fun cancelWork() {
workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}
2단계 - 취소 메서드 호출
cancelWork
를 호출하는 cancelButton
버튼을 연결합니다.
BlurActivity.kt
// In onCreate()
// Hookup the Cancel button
binding.cancelButton.setOnClickListener { viewModel.cancelWork() }
3단계 - 작업 실행 및 취소
앱을 실행합니다. 앱이 문제없이 컴파일됩니다. 사진 블러 처리를 시작한 다음 취소 버튼을 클릭합니다. 전체 체인이 취소됩니다.
이제 WorkState가 더 이상 '완료됨' 상태가 아니므로 작업이 취소되면 '진행' 버튼만 표시됩니다.
11. 작업 제약 조건
마지막으로 WorkManager
는 Constraints
를 지원합니다. Blur-O-Matic의 경우 기기를 충전해야 한다는 제약 조건을 사용합니다. 즉, 기기가 충전 중일 때만 작업 요청이 실행됩니다.
1단계 - 충전 제약 조건 만들기 및 추가
Constraints
객체를 만들려면 Constraints.Builder
를 사용합니다. 그런 다음 원하는 제약 조건을 설정하고 아래와 같이 setRequiresCharging()
메서드를 사용하여 WorkRequest
에 추가합니다.
요청이 있는 경우 androidx.work.Constraints
를 가져옵니다.
BlurViewModel.kt
// Put this inside the applyBlur() function, above the save work request.
// Create charging constraint
val constraints = Constraints.Builder()
.setRequiresCharging(true)
.build()
// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.setConstraints(constraints)
.addTag(TAG_OUTPUT)
.build()
continuation = continuation.then(save)
// Actually start the work
continuation.enqueue()
2단계 - 에뮬레이터나 기기로 테스트
이제 Blur-O-Matic을 실행할 수 있습니다. 기기를 사용하는 경우 기기를 삭제하거나 연결할 수 있습니다. 에뮬레이터를 사용하는 경우 Extended controls 창에서 충전 상태를 변경할 수 있습니다.
기기가 충전 중이 아닐 때는 SaveImageToFileWorker,
를 정지하고 기기를 연결한 후에만 실행해야 합니다.
12. 축하합니다
축하합니다. Blur-O-Matic 앱을 완료했으며 그 과정에서 다음을 배웠습니다.
- 프로젝트에 WorkManager 추가
OneTimeWorkRequest
예약- 입력 및 출력 매개변수
- 작업
WorkRequest
체이닝 - 고유
WorkRequest
체인 이름 지정 WorkRequest
태그 지정- UI에
WorkInfo
표시 WorkRequest
취소WorkRequest
에 제약 조건 추가
좋습니다. 코드의 최종 상태와 모든 변경사항을 보려면 다음을 확인하세요.
또는 원한다면 GitHub에서 WorkManager의 Codelab을 클론할 수도 있습니다.
$ git clone https://github.com/googlecodelabs/android-workmanager
WorkManager는 반복 작업, 테스트 지원 라이브러리, 병렬 작업 요청, 병합 입력을 비롯하여 이 Codelab에서 다룰 수 있는 것보다 훨씬 더 많은 것을 지원합니다. 자세히 알아보려면 WorkManager 문서를 참고하거나 고급 WorkManager Codelab을 진행하세요.