WorkManager로 백그라운드 작업

1. 시작하기 전에

이 Codelab에서는 WorkManager에 관해 알아봅니다. WorkManager는 유연하고 간편하며 이전 버전과 호환되는 라이브러리로, 지연 가능한 백그라운드 작업을 지원합니다. WorkManager는 Android에서 권장되는 작업 스케줄러로, 지연 가능한 작업을 실행하도록 보장합니다.

기본 요건

학습할 내용

실행할 작업

  • 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 앱의 코드를 찾아볼 수 있습니다.

시작 코드 실행하기

시작 코드에 익숙해지려면 다음 단계를 완료하세요.

  1. Android 스튜디오에서 스타터 코드가 있는 프로젝트를 엽니다.
  2. Android 기기나 에뮬레이터에서 앱을 실행합니다.

2bdb6fdc2567e96.png

화면에 이미지의 흐린 정도를 선택할 수 있는 라디오 버튼이 있습니다. 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라는 이미지를 가져와 몇 가지 함수를 백그라운드에서 실행합니다. 이러한 함수는 이미지를 블러 처리합니다.

  1. Android 프로젝트 창에서 com.example.bluromatic.workers 패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Kotlin Class/File을 선택합니다.
  2. 새 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 아래에 오류를 나타내는 빨간색 물결선이 표시됩니다.

9e96aa94f82c6990.png

class BlurWorker 텍스트 위로 커서를 가져가면 IDE에 오류에 관한 추가 정보가 포함된 팝업이 표시됩니다.

cdc4bbefa7a9912b.png

이 오류 메시지는 필요에 따라 doWork() 메서드를 재정의하지 않았음을 나타냅니다.

doWork() 메서드에서 표시된 컵케이크 이미지를 블러 처리하는 코드를 작성합니다.

다음 단계에 따라 오류를 수정하고 doWork() 메서드를 구현합니다.

  1. 'BlurWorker' 텍스트를 클릭하여 클래스 코드 내부에 커서를 놓습니다.
  2. Android 스튜디오 메뉴에서 Code > Override Methods...를 선택합니다.
  3. Override Members 팝업에서 doWork()를 선택합니다.
  4. OK를 클릭합니다.

8f495f0861ed19ff.png

  1. 클래스 선언 바로 앞에 TAG라는 변수를 만들고 이 변수에 BlurWorker 값을 할당합니다. 이 변수는 doWork() 메서드와 특별히 관련이 없지만 나중에 Log() 호출에서 사용합니다.

workers/BlurWorker.kt

private const val TAG = "BlurWorker"

class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
... 
  1. 작업이 실행될 때 더 잘 알 수 있으려면 WorkerUtilmakeStatusNotification() 함수를 활용해야 합니다. 이 함수를 사용하면 화면 상단에 알림 배너를 쉽게 표시할 수 있습니다.

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
    )
...
  1. 실제 이미지 블러 작업이 실행되는 return try...catch 코드 블록을 추가합니다.

workers/BlurWorker.kt

...
        makeStatusNotification(
            applicationContext.resources.getString(R.string.blurring_image),
            applicationContext
        )

        return try {
        } catch (throwable: Throwable) {
        }
...
  1. try 블록에서 Result.success() 호출을 추가합니다.
  2. catch 블록에서 Result.failure() 호출을 추가합니다.

workers/BlurWorker.kt

...
        makeStatusNotification(
            applicationContext.resources.getString(R.string.blurring_image),
            applicationContext
        )

        return try {
            Result.success()
        } catch (throwable: Throwable) {
            Result.failure()
        }
...
  1. try 블록에서 picture라는 새 변수를 만듭니다. 그런 다음 BitmapFactory.decodeResource() 메서드를 호출하고 애플리케이션의 리소스 패키지를 전달하여 반환된 비트맵 및 컵케이크 이미지의 리소스 ID로 이 변수를 채웁니다.

workers/BlurWorker.kt

...
        return try {
            val picture = BitmapFactory.decodeResource(
                applicationContext.resources,
                R.drawable.android_cupcake
            )

            Result.success()
...
  1. blurBitmap() 함수를 호출하여 비트맵을 블러 처리하고 blurLevel 매개변수의 picture 변수와 1 값을 전달합니다.
  2. 결과를 output이라는 새 변수에 저장합니다.

workers/BlurWorker.kt

...
            val picture = BitmapFactory.decodeResource(
                applicationContext.resources,
                R.drawable.android_cupcake
            )

            val output = blurBitmap(picture, 1)

            Result.success()
...
  1. 새 변수 outputUri를 만들고 writeBitmapToFile() 함수를 호출하여 이 변수를 채웁니다.
  2. writeBitmapToFile() 호출에서 애플리케이션 컨텍스트와 output 변수를 인수로 전달합니다.

workers/BlurWorker.kt

...
            val output = blurBitmap(picture, 1)

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

            Result.success()
...
  1. 사용자에게 outputUri 변수가 포함된 알림 메시지를 표시하는 코드를 추가합니다.

workers/BlurWorker.kt

...
            val outputUri = writeBitmapToFile(applicationContext, output)

            makeStatusNotification(
                "Output is $outputUri",
                applicationContext
            )

            Result.success()
...
  1. 이미지 블러 처리를 시도하는 동안 오류가 발생했음을 나타내는 오류 메시지를 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()를 호출하고 원하는 디스패처를 전달하여 변경할 수 있습니다.

  1. withContext() 블록을 만듭니다.
  2. withContext() 호출 내에서 Dispatchers.IO를 전달하여 람다 함수가 잠재적으로 IO 작업을 차단할 수 있는 특수 스레드 풀에서 실행되도록 합니다.
  3. 이전에 작성된 return try...catch 코드를 이 블록으로 이동합니다.
...
        return withContext(Dispatchers.IO) {

            return try {
                // ...
            } catch (throwable: Throwable) {
                // ...
            }
        }
...

람다 함수 내에서 return을 호출할 수 없으므로 Android 스튜디오에 다음 오류가 표시됩니다.

2d81a484b1edfd1d.png

이 오류는 팝업에 표시된 대로 라벨을 추가하여 수정할 수 있습니다.

...
            //return try {
            return@withContext try {
...

이 worker는 매우 빠르게 실행되므로, 코드에 지연을 추가하여 더 느리게 실행되는 작업을 에뮬레이션하는 것이 좋습니다.

  1. 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() 메서드 내에서 완료됩니다.

  1. 블러 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>()
}
  1. 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())
}
  1. 앱을 실행하고 Start(시작) 버튼을 클릭하면 알림이 표시됩니다.

이때 선택한 옵션과 관계없이 동일한 정도로 이미지가 흐리게 처리됩니다. 이후 단계에서 선택한 옵션에 따라 흐림 정도가 변경됩니다.

f2b3591b86d1999d.png

이미지가 성공적으로 블러 처리되었는지 확인하려면 Android 스튜디오에서 Device Explorer를 엽니다.

6bc555807e67f5ad.png

그런 다음 data > data > com.example.bluromatic > files > blur_filter_outputs > <URI>로 이동하여 컵케이크 이미지가 실제로 블러 처리되었는지 확인합니다.

fce43c920a61a2e3.png

10. 입력 데이터 및 출력 데이터

리소스 디렉터리에서 이미지 애셋을 블러 처리하는 것도 훌륭하지만, Blur-O-Matic이 진정으로 혁신적인 이미지 편집 앱이 되도록 하려면 사용자가 화면에 표시되는 이미지를 블러 처리한 다음 흐린 이미지 결과를 확인할 수 있게 해야 합니다

이렇게 하려면 WorkRequest의 입력으로 표시된 컵케이크 이미지 URI를 제공한 후 WorkRequest의 출력을 사용하여 최종 블러 처리된 이미지를 표시합니다.

ce8ec44543479fe5.png

입력 및 출력은 Data 객체를 통해 worker 안팎으로 전달됩니다. Data 객체는 키-값 쌍의 경량 컨테이너입니다. WorkRequest에서 worker 안팎으로 전달될 수 있는 소량의 데이터를 저장하기 위한 것입니다.

다음 단계에서는 입력 데이터 객체를 만들어 URI를 BlurWorker에 전달합니다.

입력 데이터 객체 만들기

  1. data/WorkManagerBluromaticRepository.kt 파일의 WorkManagerBluromaticRepository 클래스 내에서 imageUri라는 새 비공개 변수를 만듭니다.
  2. 컨텍스트 메서드 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 객체를 만듭니다. 그런 다음 imageUriblurLevel을 키-값 쌍으로 배치합니다. return builder.build()를 호출하면 데이터 객체가 만들어지고 반환됩니다.

  1. 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() 메서드 내에서 다음을 실행합니다.

  1. resourceUri라는 새 변수를 만듭니다. 그런 다음 inputData.getString()을 호출하고 입력 데이터 객체를 만들 때 키로 사용된 KEY_IMAGE_URI 상수를 전달하여 이 변수를 채웁니다.

val resourceUri = inputData.getString(KEY_IMAGE_URI)

  1. 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를 사용하여 컵케이크 이미지를 블러 처리합니다.

  1. 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 객체가 필요합니다.

  1. applicationContext 값에 contentResolver 객체를 추가합니다.

workers/BlurWorker.kt

...
    require(!resourceUri.isNullOrBlank()) {
        // ...
    }
    val resolver = applicationContext.contentResolver
...
  1. 이제 이미지 소스가 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))
    )
  1. blurBitmap() 함수 호출에서 blurLevel 변수를 전달합니다.

workers/BlurWorker.kt

//val output = blurBitmap(picture, 1)
val output = blurBitmap(picture, blurLevel)

출력 데이터 객체 만들기

이제 이 worker를 완료했으며 Result.success()에서 출력 URI를 출력 데이터 객체로 반환할 수 있습니다. 출력 URI를 출력 데이터 객체로 제공하면 다른 worker가 추가 작업을 위해 쉽게 액세스할 수 있습니다. 이 접근 방식은 다음 섹션에서 worker 체인을 만들 때 유용합니다.

이렇게 하려면 다음 단계를 완료하세요.

  1. Result.success() 코드 앞에 outputData라는 새 변수를 만듭니다.
  2. workDataOf() 함수를 호출하여 이 변수를 채우고 KEY_IMAGE_URI 상수를 키로, 변수 outputUri를 값으로 사용합니다. workDataOf() 함수는 전달된 키-값 쌍에서 데이터 객체를 만듭니다.

workers/BlurWorker.kt

import androidx.work.workDataOf
// ...
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
  1. 이 새 데이터 객체를 인수로 사용하도록 Result.success() 코드를 업데이트합니다.

workers/BlurWorker.kt

//Result.success()
Result.success(outputData)
  1. 출력 데이터 객체가 이제 URI를 사용하므로 알림을 표시하는 코드가 더 이상 필요하지 않아 이 코드를 삭제합니다.

workers/BlurWorker.kt

// REMOVE the following notification code
//makeStatusNotification(
//    "Output is $outputUri",
//    applicationContext
//)

앱 실행

이제 앱을 실행하면 컴파일될 것으로 예상할 수 있습니다. 블러 처리된 이미지를 Device Explorer를 통해 볼 수 있지만 아직 화면에서는 볼 수 없습니다.

이미지를 보려면 Synchronize가 필요할 수도 있습니다.

a658ad6e65f0ce5d.png

수고하셨습니다. WorkManager를 사용하여 입력 이미지를 블러 처리했습니다.

11. 작업 체이닝

지금은 단일 작업, 즉 이미지 블러 처리만 하고 있습니다. 이 작업은 훌륭한 첫 단계이지만 앱에는 여전히 일부 핵심 기능이 없습니다.

  • 앱이 임시 파일을 정리하지 않음
  • 앱이 이미지를 실제로 영구 파일에 저장하지 않음
  • 앱에서 사진의 블러 처리 정도가 항상 같음

WorkManager 작업 체인을 사용하여 위의 기능을 추가할 수 있습니다. WorkManager를 사용하면 순서대로 실행되거나 동시에 실행되는 별도의 WorkerRequest를 만들 수 있습니다.

이 섹션에서는 다음과 같은 작업 체인을 만듭니다.

c883bea5a5beac45.png

상자는 WorkRequest를 나타냅니다.

체이닝의 또 다른 기능은 입력을 받아서 출력을 생성하는 기능입니다. 한 WorkRequest의 출력은 체인에 있는 다음 WorkRequest의 입력이 됩니다.

이미지를 블러 처리하는 CoroutineWorker는 이미 있지만 임시 파일을 정리하는 CoroutineWorker와 이미지를 영구적으로 저장하는 CoroutineWorker가 필요합니다.

CleanupWorker 만들기

CleanupWorker는 임시 파일이 있으면 삭제합니다.

  1. Android 프로젝트 창에서 com.example.bluromatic.workers 패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Kotlin Class/File을 선택합니다.
  2. 새 Kotlin 클래스의 이름을 CleanupWorker로 지정합니다.
  3. 다음 코드 예와 같이 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입니다.

de0ee97cca135cf8.png

  1. Android 프로젝트 창에서 com.example.bluromatic.workers 패키지를 마우스 오른쪽 버튼으로 클릭하고 New > Kotlin Class/File을 선택합니다.
  2. 새 Kotlin 클래스의 이름을 SaveImageToFileWorker로 지정합니다.
  3. 다음 코드 예와 같이 SaveImageToFileWorker.kt 코드를 복사합니다.

파일 조작은 이 Codelab의 범위를 벗어나므로 SaveImageToFileWorker에 다음 코드를 복사할 수 있습니다. 제공된 코드에서 KEY_IMAGE_URI 키로 resourceUrioutput 값을 검색하고 저장하는 방법을 확인합니다. 이 프로세스는 이전에 입력 및 출력 데이터 객체용으로 작성한 코드와 매우 유사합니다.

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 체인에서 첫 번째 작업 요청은 임시 파일을 정리하는 것입니다.

  1. 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 객체를 전달하여 이 작업 요청 체인에 추가할 수 있습니다.

  1. 작업 요청 하나만 큐에 추가한 workManager.enqueue(blurBuilder.build()) 호출을 삭제합니다.
  2. .then() 메서드를 호출하여 다음 작업 요청을 체인에 추가합니다.

data/WorkManagerBluromaticRepository.kt

...
//workManager.enqueue(blurBuilder.build())

// Add the blur work request to the chain
continuation = continuation.then(blurBuilder.build())
...
  1. 이미지를 저장하고 체인에 추가하는 작업 요청을 만듭니다.

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)
...
  1. 작업을 시작하려면 연속 객체에서 enqueue() 메서드를 호출합니다.

data/WorkManagerBluromaticRepository.kt

...
continuation = continuation.then(save)

// Start the work
continuation.enqueue()
...

이 코드는 CleanupWorker WorkRequest 다음에 BlurWorker WorkRequest, 그 다음에 SaveImageToFileWorker WorkRequest가 이어지는 WorkRequest 체인을 만들고 실행합니다.

  1. 앱을 실행합니다.

이제 Start(시작)를 클릭하여 다른 worker가 실행될 때 알림을 확인할 수 있습니다. 블러 처리된 이미지를 Device Explorer에서 계속 볼 수 있으며, 다음 섹션에서는 사용자가 블러 처리된 이미지를 기기에서 확인할 수 있도록 버튼을 추가합니다.

다음 스크린샷에는 현재 실행 중인 worker가 알림 메시지에 표시됩니다.

bbe0fdd79e3bca27.png

5d43bbfff1bfebe5.png

da2d31fa3609a7b1.png

출력 폴더에는 흐림 정도가 중간인 이미지, 선택한 흐림 정도로 이미지를 표시하는 최종 이미지를 포함하여 블러 처리된 이미지가 여러 개 있습니다.

훌륭합니다. 이제 임시 파일을 정리하고 이미지를 블러 처리한 후 저장할 수 있습니다.

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로 작업 예약 문서를 참고하세요.