1. 시작하기 전에
이 Codelab에서는 WorkManager에 관해 알아봅니다. WorkManager는 유연하고 간편하며 이전 버전과 호환되는 라이브러리로, 지연 가능한 백그라운드 작업을 지원합니다. WorkManager
는 Android에서 권장되는 작업 스케줄러로, 지연 가능한 작업을 실행하도록 보장합니다.
기본 요건
- StateFlow 및 ViewModel에 관한 지식. 이러한 클래스를 처음 사용하는 경우 Compose의 ViewModel 및 상태 Codelab(특히 ViewModel 및 상태 관련) 또는 Room을 사용하여 데이터 읽기 및 업데이트 Codelab(특히 Flow 및 StateFlow 관련)을 확인하세요.
- 저장소 및 종속 항목 삽입에 관한 지식. 복습하려면 저장소 추가 및 수동 DI를 확인하세요.
- 앱에 코루틴을 구현하는 능력
학습할 내용
- 프로젝트에 WorkManager를 추가하는 방법
- 간단한 작업을 예약하는 방법
- worker의 입력 및 출력 매개변수를 구성하는 방법
- worker를 체이닝하는 방법
실행할 작업
- WorkManager를 사용하도록 시작 앱을 수정합니다.
- 이미지를 블러 처리하도록 작업 요청을 구현합니다.
- 작업을 체이닝하여 일련의 작업 그룹을 구현합니다.
- 예약 중인 작업 안팎으로 데이터를 전달합니다.
필요한 항목
- Android 스튜디오의 최신 안정화 버전
- 인터넷 연결
2. 앱 개요
요즘은 스마트폰이 사진을 정말 잘 찍습니다. 사진가가 흐릿하게 찍힌 사진을 신비로운 대상처럼 보이도록 만드는 시대는 이제 지났습니다.
이 Codelab에서는 사진을 블러 처리하여 결과를 파일에 저장하는 Blur-O-Matic 앱을 다룹니다. 네스 호의 괴물인지 장난감 잠수함인지 궁금하게 만드는 사진을 Blur-O-Matic을 통해 만들 수 있습니다.
화면에 이미지를 얼마나 흐리게 처리할지 선택할 수 있는 라디오 버튼이 표시됩니다. Start(시작) 버튼을 클릭하면 이미지가 블러 처리되어 저장됩니다.
지금은 앱이 블러를 적용하거나 최종 이미지를 저장하지 않습니다.
이 Codelab에서는 앱에 WorkManager를 추가하고, 이미지를 블러 처리할 때 생성된 임시 파일을 정리하는 worker를 만들고, 이미지를 블러 처리하고, See File(파일 보기) 버튼 클릭 시 볼 수 있는 이미지의 최종 사본을 저장하는 데 중점을 둡니다. 또한 백그라운드 작업의 상태를 모니터링하고 이에 따라 앱의 UI를 업데이트하는 방법도 알아봅니다.
3. Blur-O-Matic 시작 앱 살펴보기
시작 코드 가져오기
시작하려면 시작 코드를 다운로드하세요.
또는 코드에 관한 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 starter
이 GitHub 저장소에서 Blur-O-Matic 앱의 코드를 찾아볼 수 있습니다.
시작 코드 실행하기
시작 코드에 익숙해지려면 다음 단계를 완료하세요.
- Android 스튜디오에서 스타터 코드가 있는 프로젝트를 엽니다.
- Android 기기나 에뮬레이터에서 앱을 실행합니다.
화면에 이미지의 흐린 정도를 선택할 수 있는 라디오 버튼이 있습니다. Start(시작) 버튼을 클릭하면 앱이 이미지를 블러 처리하고 저장합니다.
지금은 Start(시작) 버튼을 클릭해도 앱이 블러를 적용하지 않습니다.
시작 코드 둘러보기
이 작업에서는 프로젝트 구조를 숙지합니다. 다음 목록을 통해 프로젝트의 중요 파일과 폴더를 둘러봅니다.
WorkerUtils
: 나중에Notifications
및 코드를 표시하고 비트맵을 파일에 저장하는 데 사용하는 편의 메서드입니다.BlurViewModel
: 이 뷰 모델은 앱 상태를 저장하고 저장소와 상호작용합니다.WorkManagerBluromaticRepository
: WorkManager로 백그라운드 작업을 시작하는 클래스입니다.Constants
: Codelab에서 사용하는 상수가 포함된 정적 클래스입니다.BluromaticScreen
: UI의 구성 가능한 함수를 포함하고BlurViewModel
과 상호작용합니다. 구성 가능한 함수는 이미지를 표시하고 원하는 흐림 수준을 선택하는 라디오 버튼을 포함합니다.
4. WorkManager란?
WorkManager는 상황별 실행과 보장된 실행을 조합하여 적용해야 하는 백그라운드 작업을 위한 아키텍처 구성요소로서 Android Jetpack의 일부입니다. 상황별 실행을 적용하면 WorkManager가 최대한 빨리 백그라운드 작업을 실행합니다. 보장된 실행을 적용하면 WorkManager가 사용자가 앱을 벗어난 경우를 비롯한 다양한 상황에서 로직을 처리하여 작업을 시작합니다.
WorkManager는 매우 유연한 라이브러리로, 다음과 같은 이점이 있습니다.
- 비동기 일회성 작업과 주기적인 작업 모두 지원
- 네트워크 상태, 저장공간, 충전 상태와 같은 제약 조건 지원
- 동시 작업 실행과 같은 복잡한 작업 요청 체이닝
- 한 작업 요청의 출력이 다음 작업 요청의 입력으로 사용됨
- API 수준 14까지 호환됨(참고 확인)
- Google Play 서비스를 사용하거나 사용하지 않고 작업
- 시스템 상태 권장사항 준수
- 앱 UI에 작업 요청의 상태를 쉽게 표시할 수 있도록 지원
5. WorkManager가 적합한 작업
여기서 완료해야 하는 작업에 WorkManager 라이브러리가 적합합니다. 이러한 작업의 실행은 작업이 큐에 추가된 후 계속 실행되는 앱에 종속되지 않습니다. 앱이 닫히거나 사용자가 홈 화면으로 돌아오더라도 작업이 실행됩니다.
WorkManager는 아래와 같은 작업에 사용하는 것이 적합합니다.
- 주기적으로 최신 뉴스 기사 쿼리
- 이미지에 필터를 적용한 다음 이미지 저장
- 주기적으로 로컬 데이터를 네트워크와 동기화
WorkManager는 기본 스레드에서 작업을 실행하는 한 가지 옵션이지만 기본 스레드에서 모든 유형의 작업을 실행하기 위한 포괄적인 옵션은 아닙니다. 코루틴은 이전 Codelab에서 다루는 또 다른 옵션입니다.
어떤 작업에 WorkManager를 사용할지 자세히 알아보려면 백그라운드 작업 가이드를 참고하세요.
6. 앱에 WorkManager 추가
WorkManager
에는 다음과 같은 Gradle 종속 항목이 필요합니다. 이러한 종속 항목은 빌드 파일에 이미 포함되어 있습니다.
app/build.gradle.kts를 참고하세요.
dependencies {
// WorkManager dependency
implementation("androidx.work:work-runtime-ktx:2.8.1")
}
앱에서 최신 안정화 버전의 work-runtime-ktx
를 사용해야 합니다.
버전을 변경하는 경우 Sync Now(지금 동기화)를 클릭하여 업데이트된 Gradle 파일로 프로젝트를 동기화합니다.
7. WorkManager 기본사항
알아야 할 몇 가지 WorkManager 클래스가 있습니다.
Worker
/CoroutineWorker
: worker는 백그라운드 스레드에서 동기식으로 작업을 실행하는 클래스입니다. 우리는 비동기 작업에 관심이 있으므로 Kotlin 코루틴과 상호 운용되는 CoroutineWorker를 사용할 수 있습니다. 이 앱에서는 CoroutineWorker 클래스에서 확장하고doWork()
메서드를 재정의합니다. 백그라운드에서 실행하고자 하는 실제 작업의 코드를 이 메서드에 입력합니다.WorkRequest
: 이 클래스는 작업 실행 요청을 나타냅니다.WorkRequest
에서는 worker를 한 번 또는 주기적으로 실행해야 하는지 정의합니다. 제약 조건은 작업 실행 전에 특정 조건 충족을 요구하는WorkRequest
에 배치될 수도 있습니다. 한 가지 예는 요청된 작업을 시작하기 전에 기기를 충전하는 것입니다.WorkRequest
를 만드는 과정에서CoroutineWorker
를 전달합니다.WorkManager
: 이 클래스는 실제로WorkRequest
를 예약하고 실행합니다. 지정된 제약 조건을 준수하면서 시스템 리소스에 부하를 분산하는 방식으로WorkRequest
를 예약합니다.
이 경우 이미지를 블러 처리하는 코드가 포함된 새 BlurWorker
클래스를 정의합니다. Start(시작) 버튼을 클릭하면 WorkManager가 WorkRequest
객체를 만든 후에 큐에 추가합니다.
8. BlurWorker 만들기
이 단계에서는 res/drawable
폴더의 android_cupcake.png
라는 이미지를 가져와 몇 가지 함수를 백그라운드에서 실행합니다. 이러한 함수는 이미지를 블러 처리합니다.
- Android 프로젝트 창에서
com.example.bluromatic.workers
패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Kotlin Class/File을 선택합니다. - 새 Kotlin 클래스의 이름을
BlurWorker
로 지정합니다. 필수 생성자 매개변수를 사용하여 이 클래스를CoroutineWorker
에서 확장합니다.
workers/BlurWorker.kt
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import android.content.Context
class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
}
BlurWorker
클래스는 더 일반적인 Worker
클래스 대신 CoroutineWorker
클래스를 확장합니다. doWork()
의 CoroutineWorker
클래스 구현은 Worker
로 실행할 수 없는 비동기 코드를 실행할 수 있는 정지 함수입니다. WorkManager의 스레딩 가이드에 자세히 설명된 대로 'CoroutineWorker는 Kotlin 사용자에게 권장되는 구현입니다'.
이제 Android 스튜디오에서 class BlurWorker
아래에 오류를 나타내는 빨간색 물결선이 표시됩니다.
class BlurWorker
텍스트 위로 커서를 가져가면 IDE에 오류에 관한 추가 정보가 포함된 팝업이 표시됩니다.
이 오류 메시지는 필요에 따라 doWork()
메서드를 재정의하지 않았음을 나타냅니다.
doWork()
메서드에서 표시된 컵케이크 이미지를 블러 처리하는 코드를 작성합니다.
다음 단계에 따라 오류를 수정하고 doWork()
메서드를 구현합니다.
- 'BlurWorker' 텍스트를 클릭하여 클래스 코드 내부에 커서를 놓습니다.
- Android 스튜디오 메뉴에서 Code > Override Methods...를 선택합니다.
- Override Members 팝업에서
doWork()
를 선택합니다. - OK를 클릭합니다.
- 클래스 선언 바로 앞에
TAG
라는 변수를 만들고 이 변수에BlurWorker
값을 할당합니다. 이 변수는doWork()
메서드와 특별히 관련이 없지만 나중에Log()
호출에서 사용합니다.
workers/BlurWorker.kt
private const val TAG = "BlurWorker"
class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
...
- 작업이 실행될 때 더 잘 알 수 있으려면
WorkerUtil
의makeStatusNotification()
함수를 활용해야 합니다. 이 함수를 사용하면 화면 상단에 알림 배너를 쉽게 표시할 수 있습니다.
doWork()
메서드 내에서 makeStatusNotification()
함수를 사용하여 상태 알림을 표시하고 블러 worker가 시작되어 이미지를 블러 처리한다고 사용자에게 알립니다.
workers/BlurWorker.kt
import com.example.bluromatic.R
...
override suspend fun doWork(): Result {
makeStatusNotification(
applicationContext.resources.getString(R.string.blurring_image),
applicationContext
)
...
- 실제 이미지 블러 작업이 실행되는
return try...catch
코드 블록을 추가합니다.
workers/BlurWorker.kt
...
makeStatusNotification(
applicationContext.resources.getString(R.string.blurring_image),
applicationContext
)
return try {
} catch (throwable: Throwable) {
}
...
try
블록에서Result.success()
호출을 추가합니다.catch
블록에서Result.failure()
호출을 추가합니다.
workers/BlurWorker.kt
...
makeStatusNotification(
applicationContext.resources.getString(R.string.blurring_image),
applicationContext
)
return try {
Result.success()
} catch (throwable: Throwable) {
Result.failure()
}
...
try
블록에서picture
라는 새 변수를 만듭니다. 그런 다음BitmapFactory.decodeResource
()
메서드를 호출하고 애플리케이션의 리소스 패키지를 전달하여 반환된 비트맵 및 컵케이크 이미지의 리소스 ID로 이 변수를 채웁니다.
workers/BlurWorker.kt
...
return try {
val picture = BitmapFactory.decodeResource(
applicationContext.resources,
R.drawable.android_cupcake
)
Result.success()
...
blurBitmap()
함수를 호출하여 비트맵을 블러 처리하고blurLevel
매개변수의picture
변수와1
값을 전달합니다.- 결과를
output
이라는 새 변수에 저장합니다.
workers/BlurWorker.kt
...
val picture = BitmapFactory.decodeResource(
applicationContext.resources,
R.drawable.android_cupcake
)
val output = blurBitmap(picture, 1)
Result.success()
...
- 새 변수
outputUri
를 만들고writeBitmapToFile()
함수를 호출하여 이 변수를 채웁니다. writeBitmapToFile()
호출에서 애플리케이션 컨텍스트와output
변수를 인수로 전달합니다.
workers/BlurWorker.kt
...
val output = blurBitmap(picture, 1)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(applicationContext, output)
Result.success()
...
- 사용자에게
outputUri
변수가 포함된 알림 메시지를 표시하는 코드를 추가합니다.
workers/BlurWorker.kt
...
val outputUri = writeBitmapToFile(applicationContext, output)
makeStatusNotification(
"Output is $outputUri",
applicationContext
)
Result.success()
...
- 이미지 블러 처리를 시도하는 동안 오류가 발생했음을 나타내는 오류 메시지를
catch
블록에 기록합니다.Log.e()
호출은 이전에 정의된TAG
변수, 적절한 메시지, 발생한 예외를 전달합니다.
workers/BlurWorker.kt
...
} catch (throwable: Throwable) {
Log.e(
TAG,
applicationContext.resources.getString(R.string.error_applying_blur),
throwable
)
Result.failure()
}
...
CoroutineWorker,
는 기본적으로 Dispatchers.Default
로 실행되지만 withContext()
를 호출하고 원하는 디스패처를 전달하여 변경할 수 있습니다.
withContext()
블록을 만듭니다.withContext()
호출 내에서Dispatchers.IO
를 전달하여 람다 함수가 잠재적으로 IO 작업을 차단할 수 있는 특수 스레드 풀에서 실행되도록 합니다.- 이전에 작성된
return try...catch
코드를 이 블록으로 이동합니다.
...
return withContext(Dispatchers.IO) {
return try {
// ...
} catch (throwable: Throwable) {
// ...
}
}
...
람다 함수 내에서 return
을 호출할 수 없으므로 Android 스튜디오에 다음 오류가 표시됩니다.
이 오류는 팝업에 표시된 대로 라벨을 추가하여 수정할 수 있습니다.
...
//return try {
return@withContext try {
...
이 worker는 매우 빠르게 실행되므로, 코드에 지연을 추가하여 더 느리게 실행되는 작업을 에뮬레이션하는 것이 좋습니다.
withContext()
람다 내에서delay()
유틸리티 함수 호출을 추가하고DELAY_TIME_MILLIS
상수를 전달합니다. 이 호출은 Codelab에서 알림 메시지 간 지연을 제공하기 위한 목적으로만 사용됩니다.
import com.example.bluromatic.DELAY_TIME_MILLIS
import kotlinx.coroutines.delay
...
return withContext(Dispatchers.IO) {
// This is an utility function added to emulate slower work.
delay(DELAY_TIME_MILLIS)
val picture = BitmapFactory.decodeResource(
...
9. WorkManagerBluromaticRepository 업데이트
저장소는 WorkManager와의 모든 상호작용을 처리합니다. 이 구조는 관심사 분리 디자인 원칙을 따르며 권장되는 Android 아키텍처 패턴입니다.
data/WorkManagerBluromaticRepository.kt
파일의WorkManagerBluromaticRepository
클래스 내에서workManager
라는 비공개 변수를 만들고WorkManager.getInstance(context)
를 호출하여WorkManager
인스턴스를 저장합니다.
data/WorkManagerBluromaticRepository.kt
import androidx.work.WorkManager
...
class WorkManagerBluromaticRepository(context: Context) : BluromaticRepository {
// New code
private val workManager = WorkManager.getInstance(context)
...
WorkManager에서 WorkRequest를 만들고 큐에 추가
이제 WorkRequest
를 만들고 WorkManager에 실행하도록 지시합니다. 두 가지 WorkRequest
유형이 있습니다.
OneTimeWorkRequest
: 한 번만 실행되는WorkRequest
입니다.PeriodicWorkRequest
: 일정 주기로 반복적으로 실행되는WorkRequest
입니다.
Start(시작) 버튼 클릭 시 한 번만 이미지를 블러 처리하도록 설정합니다.
이 작업은 Start(시작) 버튼 클릭 시 호출되는 applyBlur()
메서드에서 실행됩니다.
다음 단계는 applyBlur()
메서드 내에서 완료됩니다.
- 블러 worker용
OneTimeWorkRequest
를 만들고 WorkManager KTX에서OneTimeWorkRequestBuilder
확장 함수를 호출하여blurBuilder
라는 새 변수를 채웁니다.
data/WorkManagerBluromaticRepository.kt
import com.example.bluromatic.workers.BlurWorker
import androidx.work.OneTimeWorkRequestBuilder
...
override fun applyBlur(blurLevel: Int) {
// Create WorkRequest to blur the image
val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
}
workManager
객체에서enqueue()
메서드를 호출하여 작업을 시작합니다.
data/WorkManagerBluromaticRepository.kt
import com.example.bluromatic.workers.BlurWorker
import androidx.work.OneTimeWorkRequestBuilder
...
override fun applyBlur(blurLevel: Int) {
// Create WorkRequest to blur the image
val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
// Start the work
workManager.enqueue(blurBuilder.build())
}
- 앱을 실행하고 Start(시작) 버튼을 클릭하면 알림이 표시됩니다.
이때 선택한 옵션과 관계없이 동일한 정도로 이미지가 흐리게 처리됩니다. 이후 단계에서 선택한 옵션에 따라 흐림 정도가 변경됩니다.
이미지가 성공적으로 블러 처리되었는지 확인하려면 Android 스튜디오에서 Device Explorer를 엽니다.
그런 다음 data > data > com.example.bluromatic > files > blur_filter_outputs > <URI>로 이동하여 컵케이크 이미지가 실제로 블러 처리되었는지 확인합니다.
10. 입력 데이터 및 출력 데이터
리소스 디렉터리에서 이미지 애셋을 블러 처리하는 것도 훌륭하지만, Blur-O-Matic이 진정으로 혁신적인 이미지 편집 앱이 되도록 하려면 사용자가 화면에 표시되는 이미지를 블러 처리한 다음 흐린 이미지 결과를 확인할 수 있게 해야 합니다
이렇게 하려면 WorkRequest
의 입력으로 표시된 컵케이크 이미지 URI를 제공한 후 WorkRequest
의 출력을 사용하여 최종 블러 처리된 이미지를 표시합니다.
입력 및 출력은 Data
객체를 통해 worker 안팎으로 전달됩니다. Data
객체는 키-값 쌍의 경량 컨테이너입니다. WorkRequest
에서 worker 안팎으로 전달될 수 있는 소량의 데이터를 저장하기 위한 것입니다.
다음 단계에서는 입력 데이터 객체를 만들어 URI를 BlurWorker
에 전달합니다.
입력 데이터 객체 만들기
data/WorkManagerBluromaticRepository.kt
파일의WorkManagerBluromaticRepository
클래스 내에서imageUri
라는 새 비공개 변수를 만듭니다.- 컨텍스트 메서드
getImageUri()
를 호출하여 변수를 이미지 URI로 채웁니다.
data/WorkManagerBluromaticRepository.kt
import com.example.bluromatic.getImageUri
...
class WorkManagerBluromaticRepository(context: Context) : BluromaticRepository {
private var imageUri: Uri = context.getImageUri() // <- Add this
private val workManager = WorkManager.getInstance(context)
...
앱 코드에는 입력 데이터 객체를 만들기 위한 createInputDataForWorkRequest()
도우미 함수가 포함되어 있습니다.
data/WorkManagerBluromaticRepository.kt
// For reference - already exists in the app
private fun createInputDataForWorkRequest(blurLevel: Int, imageUri: Uri): Data {
val builder = Data.Builder()
builder.putString(KEY_IMAGE_URI, imageUri.toString()).putInt(BLUR_LEVEL, blurLevel)
return builder.build()
}
이 도우미 함수는 먼저 Data.Builder
객체를 만듭니다. 그런 다음 imageUri
및 blurLevel
을 키-값 쌍으로 배치합니다. return builder.build()
를 호출하면 데이터 객체가 만들어지고 반환됩니다.
- WorkRequest의 입력 데이터 객체를 설정하려면
blurBuilder.setInputData()
메서드를 호출합니다.createInputDataForWorkRequest()
도우미 함수를 인수로 호출하여 한 번에 데이터 객체를 만들고 전달할 수 있습니다.createInputDataForWorkRequest()
함수 호출의 경우blurLevel
변수와imageUri
변수를 전달합니다.
data/WorkManagerBluromaticRepository.kt
override fun applyBlur(blurLevel: Int) {
// Create WorkRequest to blur the image
val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
// New code for input data object
blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))
workManager.enqueue(blurBuilder.build())
}
입력 데이터 객체에 액세스
이제 BlurWorker
클래스의 doWork()
메서드를 업데이트하여 입력 데이터 객체에 의해 전달된 URI 및 흐림 수준을 가져옵니다. blurLevel
의 값이 제공되지 않는 경우 기본값은 1
입니다.
doWork()
메서드 내에서 다음을 실행합니다.
resourceUri
라는 새 변수를 만듭니다. 그런 다음inputData.getString()
을 호출하고 입력 데이터 객체를 만들 때 키로 사용된KEY_IMAGE_URI
상수를 전달하여 이 변수를 채웁니다.
val resourceUri = inputData.getString(KEY_IMAGE_URI)
blurLevel
이라는 새 변수를 만듭니다.inputData.getInt()
를 호출하고 입력 데이터 객체를 만들 때 키로 사용된BLUR_LEVEL
상수를 전달하여 변수를 채웁니다. 이 키-값 쌍이 생성되지 않은 경우를 위해 기본값1
을 제공합니다.
workers/BlurWorker.kt
import com.example.bluromatic.KEY_BLUR_LEVEL
import com.example.bluromatic.KEY_IMAGE_URI
...
override fun doWork(): Result {
// ADD THESE LINES
val resourceUri = inputData.getString(KEY_IMAGE_URI)
val blurLevel = inputData.getInt(KEY_BLUR_LEVEL, 1)
// ... rest of doWork()
}
이제 URI를 사용하여 컵케이크 이미지를 블러 처리합니다.
resourceUri
변수가 채워졌는지 확인합니다. 채워지지 않은 경우 코드에서 예외가 발생합니다. 그 뒤에 오는 코드는 첫 번째 인수가 false로 평가되면IllegalArgumentException
이 발생하는require()
문을 사용합니다.
workers/BlurWorker.kt
return@withContext try {
// NEW code
require(!resourceUri.isNullOrBlank()) {
val errorMessage =
applicationContext.resources.getString(R.string.invalid_input_uri)
Log.e(TAG, errorMessage)
errorMessage
}
이미지 소스가 URI로 전달되므로 URI가 가리키는 콘텐츠를 읽으려면 ContentResolver 객체가 필요합니다.
applicationContext
값에contentResolver
객체를 추가합니다.
workers/BlurWorker.kt
...
require(!resourceUri.isNullOrBlank()) {
// ...
}
val resolver = applicationContext.contentResolver
...
- 이제 이미지 소스가 URI에 전달되므로
BitmapFactory.decodeResource()
대신BitmapFactory.decodeStream()
을 사용하여 비트맵 객체를 만듭니다.
workers/BlurWorker.kt
import android.net.Uri
...
// val picture = BitmapFactory.decodeResource(
// applicationContext.resources,
// R.drawable.android_cupcake
// )
val resolver = applicationContext.contentResolver
val picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri))
)
blurBitmap()
함수 호출에서blurLevel
변수를 전달합니다.
workers/BlurWorker.kt
//val output = blurBitmap(picture, 1)
val output = blurBitmap(picture, blurLevel)
출력 데이터 객체 만들기
이제 이 worker를 완료했으며 Result.success()
에서 출력 URI를 출력 데이터 객체로 반환할 수 있습니다. 출력 URI를 출력 데이터 객체로 제공하면 다른 worker가 추가 작업을 위해 쉽게 액세스할 수 있습니다. 이 접근 방식은 다음 섹션에서 worker 체인을 만들 때 유용합니다.
이렇게 하려면 다음 단계를 완료하세요.
Result.success()
코드 앞에outputData
라는 새 변수를 만듭니다.workDataOf()
함수를 호출하여 이 변수를 채우고KEY_IMAGE_URI
상수를 키로, 변수outputUri
를 값으로 사용합니다.workDataOf()
함수는 전달된 키-값 쌍에서 데이터 객체를 만듭니다.
workers/BlurWorker.kt
import androidx.work.workDataOf
// ...
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
- 이 새 데이터 객체를 인수로 사용하도록
Result.success()
코드를 업데이트합니다.
workers/BlurWorker.kt
//Result.success()
Result.success(outputData)
- 출력 데이터 객체가 이제 URI를 사용하므로 알림을 표시하는 코드가 더 이상 필요하지 않아 이 코드를 삭제합니다.
workers/BlurWorker.kt
// REMOVE the following notification code
//makeStatusNotification(
// "Output is $outputUri",
// applicationContext
//)
앱 실행
이제 앱을 실행하면 컴파일될 것으로 예상할 수 있습니다. 블러 처리된 이미지를 Device Explorer를 통해 볼 수 있지만 아직 화면에서는 볼 수 없습니다.
이미지를 보려면 Synchronize가 필요할 수도 있습니다.
수고하셨습니다. WorkManager
를 사용하여 입력 이미지를 블러 처리했습니다.
11. 작업 체이닝
지금은 단일 작업, 즉 이미지 블러 처리만 하고 있습니다. 이 작업은 훌륭한 첫 단계이지만 앱에는 여전히 일부 핵심 기능이 없습니다.
- 앱이 임시 파일을 정리하지 않음
- 앱이 이미지를 실제로 영구 파일에 저장하지 않음
- 앱에서 사진의 블러 처리 정도가 항상 같음
WorkManager 작업 체인을 사용하여 위의 기능을 추가할 수 있습니다. WorkManager를 사용하면 순서대로 실행되거나 동시에 실행되는 별도의 WorkerRequest
를 만들 수 있습니다.
이 섹션에서는 다음과 같은 작업 체인을 만듭니다.
상자는 WorkRequest
를 나타냅니다.
체이닝의 또 다른 기능은 입력을 받아서 출력을 생성하는 기능입니다. 한 WorkRequest
의 출력은 체인에 있는 다음 WorkRequest
의 입력이 됩니다.
이미지를 블러 처리하는 CoroutineWorker
는 이미 있지만 임시 파일을 정리하는 CoroutineWorker
와 이미지를 영구적으로 저장하는 CoroutineWorker
가 필요합니다.
CleanupWorker 만들기
CleanupWorker
는 임시 파일이 있으면 삭제합니다.
- Android 프로젝트 창에서
com.example.bluromatic.workers
패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Kotlin Class/File을 선택합니다. - 새 Kotlin 클래스의 이름을
CleanupWorker
로 지정합니다. - 다음 코드 예와 같이 CleanupWorker.kt용 코드를 복사합니다.
파일 조작은 이 Codelab의 범위를 벗어나므로 CleanupWorker
에 다음 코드를 복사할 수 있습니다.
workers/CleanupWorker.kt
package com.example.bluromatic.workers
import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.example.bluromatic.DELAY_TIME_MILLIS
import com.example.bluromatic.OUTPUT_PATH
import com.example.bluromatic.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File
/**
* Cleans up temporary files generated during blurring process
*/
private const val TAG = "CleanupWorker"
class CleanupWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend 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(
applicationContext.resources.getString(R.string.cleaning_up_files),
applicationContext
)
return withContext(Dispatchers.IO) {
delay(DELAY_TIME_MILLIS)
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()
}
}
}
}
SaveImageToFileWorker 만들기
SaveImageToFileWorker
클래스는 임시 파일을 영구 파일에 저장합니다.
SaveImageToFileWorker
는 입력과 출력을 처리합니다. 입력은 KEY_IMAGE_URI
키로 저장된 임시 블러 처리된 이미지 URI의 String
입니다. 출력은 KEY_IMAGE_URI
키로 저장된 블러 처리된 이미지 URI의 String
입니다.
- Android 프로젝트 창에서
com.example.bluromatic.workers
패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Kotlin Class/File을 선택합니다. - 새 Kotlin 클래스의 이름을
SaveImageToFileWorker
로 지정합니다. - 다음 코드 예와 같이 SaveImageToFileWorker.kt 코드를 복사합니다.
파일 조작은 이 Codelab의 범위를 벗어나므로 SaveImageToFileWorker
에 다음 코드를 복사할 수 있습니다. 제공된 코드에서 KEY_IMAGE_URI
키로 resourceUri
및 output
값을 검색하고 저장하는 방법을 확인합니다. 이 프로세스는 이전에 입력 및 출력 데이터 객체용으로 작성한 코드와 매우 유사합니다.
workers/SaveImageToFileWorker.kt
package com.example.bluromatic.workers
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.example.bluromatic.DELAY_TIME_MILLIS
import com.example.bluromatic.KEY_IMAGE_URI
import com.example.bluromatic.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Date
/**
* Saves the image to a permanent file
*/
private const val TAG = "SaveImageToFileWorker"
class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
private val title = "Blurred Image"
private val dateFormatter = SimpleDateFormat(
"yyyy.MM.dd 'at' HH:mm:ss z",
Locale.getDefault()
)
override suspend 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(
applicationContext.resources.getString(R.string.saving_image),
applicationContext
)
return withContext(Dispatchers.IO) {
delay(DELAY_TIME_MILLIS)
val resolver = applicationContext.contentResolver
return@withContext 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,
applicationContext.resources.getString(R.string.writing_to_mediaStore_failed)
)
Result.failure()
}
} catch (exception: Exception) {
Log.e(
TAG,
applicationContext.resources.getString(R.string.error_saving_image),
exception
)
Result.failure()
}
}
}
}
작업 체인 만들기
현재 코드는 단일 WorkRequest
만 만들고 실행합니다.
이 단계에서는 하나의 블러 이미지 요청 대신 WorkRequest 체인을 만들고 실행하도록 코드를 수정합니다.
WorkRequest 체인에서 첫 번째 작업 요청은 임시 파일을 정리하는 것입니다.
OneTimeWorkRequestBuilder
를 호출하는 대신workManager.beginWith()
를 호출합니다.
beginWith()
메서드를 호출하면 WorkContinuation
객체가 반환되고 체인의 첫 번째 작업 요청이 있는 WorkRequest
체인의 시작점이 생성됩니다.
data/WorkManagerBluromaticRepository.kt
import androidx.work.OneTimeWorkRequest
import com.example.bluromatic.workers.CleanupWorker
// ...
override 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 blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
...
then()
메서드를 호출하고 WorkRequest
객체를 전달하여 이 작업 요청 체인에 추가할 수 있습니다.
- 작업 요청 하나만 큐에 추가한
workManager.enqueue(blurBuilder.build())
호출을 삭제합니다. .then()
메서드를 호출하여 다음 작업 요청을 체인에 추가합니다.
data/WorkManagerBluromaticRepository.kt
...
//workManager.enqueue(blurBuilder.build())
// Add the blur work request to the chain
continuation = continuation.then(blurBuilder.build())
...
- 이미지를 저장하고 체인에 추가하는 작업 요청을 만듭니다.
data/WorkManagerBluromaticRepository.kt
import com.example.bluromatic.workers.SaveImageToFileWorker
...
continuation = continuation.then(blurBuilder.build())
// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.build()
continuation = continuation.then(save)
...
- 작업을 시작하려면 연속 객체에서
enqueue()
메서드를 호출합니다.
data/WorkManagerBluromaticRepository.kt
...
continuation = continuation.then(save)
// Start the work
continuation.enqueue()
...
이 코드는 CleanupWorker
WorkRequest
다음에 BlurWorker
WorkRequest
, 그 다음에 SaveImageToFileWorker
WorkRequest
가 이어지는 WorkRequest 체인을 만들고 실행합니다.
- 앱을 실행합니다.
이제 Start(시작)를 클릭하여 다른 worker가 실행될 때 알림을 확인할 수 있습니다. 블러 처리된 이미지를 Device Explorer에서 계속 볼 수 있으며, 다음 섹션에서는 사용자가 블러 처리된 이미지를 기기에서 확인할 수 있도록 버튼을 추가합니다.
다음 스크린샷에는 현재 실행 중인 worker가 알림 메시지에 표시됩니다.
출력 폴더에는 흐림 정도가 중간인 이미지, 선택한 흐림 정도로 이미지를 표시하는 최종 이미지를 포함하여 블러 처리된 이미지가 여러 개 있습니다.
훌륭합니다. 이제 임시 파일을 정리하고 이미지를 블러 처리한 후 저장할 수 있습니다.
12. 솔루션 코드 가져오기
완료된 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 intermediate
또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.
이 Codelab의 솔루션 코드는 GitHub에서 확인하세요.
13. 결론
축하합니다. Blur-O-Matic 앱을 완료했으며 그 과정에서 다음을 배웠습니다.
- 프로젝트에 WorkManager 추가
OneTimeWorkRequest
예약- 입력 및 출력 매개변수
- 작업
WorkRequest
체이닝
WorkManager는 반복 작업, 테스트 지원 라이브러리, 병렬 작업 요청, 병합 입력을 비롯하여 이 Codelab에서 다룰 수 있는 것보다 훨씬 더 많은 것을 지원합니다.
자세한 내용은 WorkManager로 작업 예약 문서를 참고하세요.