WorkManager를 사용한 백그라운드 작업 - Kotlin

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을 통해 만들 수 있습니다.

컵케이크의 자리표시자 이미지, 이미지에 적용할 블러 처리 옵션 3개, 버튼 2개가 포함된 완료된 상태의 앱 이미지. 하나는 이미지 블러 처리를 시작하기 위한 버튼, 다른 하나는 블러 처리된 이미지를 보기 위한 버튼.

'파일 보기'를 클릭한 후 보이는 블러 처리된 이미지.

학습할 내용

  • 프로젝트에 WorkManager 추가
  • 단순한 작업 예약
  • 입력 및 출력 매개변수
  • 작업 체이닝
  • 고유 작업
  • UI에 작업 상태 표시
  • 작업 취소
  • 작업 제약 조건

필요한 항목

2. 설정

1단계 - 코드 다운로드

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

또는 원한다면 GitHub에서 WorkManager Codelab을 클론할 수도 있습니다.

$ git clone -b start_kotlin https://github.com/googlecodelabs/android-workmanager

2단계 - 앱 실행

앱을 실행합니다. 다음과 같은 화면이 표시됩니다.

9e4707e0fbdd93c7.png

화면에는 이미지를 얼마나 블러 처리할지 선택할 수 있는 라디오 버튼이 있습니다. 적용 버튼을 누르면 최종적으로 이미지가 블러 처리되어 저장됩니다.

지금은 앱이 블러를 적용하지 않습니다.

시작 코드에는 다음이 포함됩니다.

  • 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단계 - 생성자 추가

WorkerBlurWorker 클래스의 종속 항목을 추가합니다.

class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
}

3단계 - doWork() 재정의 및 구현

Worker가 표시된 컵케이크 이미지를 블러 처리합니다.

작업이 언제 실행되는지 더 잘 확인하기 위해 WorkerUtil의 makeStatusNotification()을 사용합니다. 이 메서드를 사용하면 화면 상단에 알림 배너를 쉽게 표시할 수 있습니다.

doWork() 메서드를 재정의하고 다음을 구현합니다. 섹션 끝에서 완성된 코드를 참고할 수 있습니다.

  1. applicationContext 속성을 호출하여 Context를 가져옵니다. 이를 appContext이라는 새 val에 할당합니다. 처리할 다양한 비트맵 조작을 위해 필요합니다.
  2. 사용자에게 이미지 블러 처리에 관해 알리는 makeStatusNotification 함수를 사용하여 상태 알림을 표시합니다.
  3. 컵케이크 이미지에서 Bitmap을 만듭니다.
val picture = BitmapFactory.decodeResource(
        appContext.resources,
        R.drawable.android_cupcake)
  1. WorkerUtils에서 blurBitmap 메서드를 호출하여 블러 처리된 버전의 비트맵을 가져옵니다.
  2. WorkerUtils에서 writeBitmapToFile 메서드를 호출하여 이 비트맵을 임시 파일에 씁니다. 반환된 URI를 로컬 변수에 저장해야 합니다.
  3. WorkerUtils에서 makeStatusNotification 메서드를 호출하여 URI를 표시하는 알림을 만듭니다.
  4. Result.success()를 반환합니다.
  5. 3~6단계의 코드를 try/catch 문으로 래핑합니다. 일반 Throwable을 포착합니다.
  6. catch 문에서 로그 구문 Log.e(TAG, "Error applying blur")를 사용하여 오류 메시지를 출력합니다.
  7. 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단계 - 코드 실행

코드를 실행합니다. 컴파일되고 진행 버튼을 누르면 알림이 표시됩니다. 더 흐리게 처리된 결과를 보기 위해 '더 흐리게 처리' 또는 '가장 흐리게 처리' 옵션을 선택해야 합니다.

ed497b57e1f527be.png

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

cf10a1af6e84f5ff.png

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

e1f61035d680ba03.png

5. 입력 및 출력 추가

리소스 디렉터리에서 이미지 애셋 블러 처리가 잘 구현되었습니다. 그러나 Blur-O-Matic이 추구하는 진정으로 획기적인 이미지 수정 앱이 되도록 하려면 사용자가 화면에 표시되는 이미지를 블러 처리할 수 있게 한 후, 사용자에게 블러 처리된 결과를 보여줄 수 있어야 합니다.

그러기 위해서 표시된 WorkRequest입력으로 표시된 컵케이크 이미지의 URI를 제공한 다음 WorkRequest의 출력을 사용하여 최종 블러 처리된 이미지를 표시합니다.

1단계 - 데이터 입력 객체 만들기

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

사용자 이미지의 URI를 번들로 전달할 것입니다. 이 URI는 imageUri라는 변수에 저장됩니다.

BlurViewModel에서 createInputDataForUri라는 비공개 메서드를 만듭니다. 이 메서드는 다음과 같은 역할을 합니다.

  1. Data.Builder 객체를 만듭니다. 요청이 있는 경우 androidx.work.Data를 가져옵니다.
  2. imageUri가 null이 아닌 URI이면 putString 메서드를 사용하여 Data 객체에 추가합니다. 이 메서드는 키와 값을 사용합니다. Constants 클래스의 문자열 상수 KEY_IMAGE_URI를 사용할 수 있습니다.
  3. 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에 데이터 객체 전달

다음을 수행하도록 BlurViewModelapplyBlur 메서드를 변경합니다.

  1. OneTimeWorkRequestBuilder를 만듭니다.
  2. setInputData를 호출하여 createInputDataForUri의 결과를 전달합니다.
  3. OneTimeWorkRequest를 빌드합니다.
  4. 작업 실행이 예약되도록 WorkManager 요청을 사용하여 작업 요청을 큐에 추가합니다.

다음은 완료된 applyBlur 메서드입니다.

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
    val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
            .setInputData(createInputDataForUri())
            .build()

    workManager.enqueue(blurRequest)
}

3단계 - BlurWorker의 doWork()를 업데이트하여 입력 가져오기

이제 전달한 URI를 Data 객체에서 가져오도록 BlurWorkerdoWork() 메서드를 업데이트하겠습니다.

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

  1. 이미지 리소스를 가져온 이전 코드를 삭제합니다.

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

  1. 전달된 Data에서 얻은 resourceUri가 비어 있지 않은지 확인합니다.
  2. 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를 출력 데이터로 제공합니다. 이렇게 하면 다음 장에서 작업자 체인을 만들 때 유용합니다. 방법은 다음과 같습니다.

  1. Data를 만들고 입력의 경우와 마찬가지로 outputUriString으로 저장합니다. 같은 키(KEY_IMAGE_URI)를 사용합니다.
  2. 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>로 이동합니다.

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

7e717ffd6b3d9d52.png

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

6. 작업 체이닝

지금은 단일 작업, 즉 이미지 블러 처리만 하고 있습니다. 훌륭한 첫 단계이지만 일부 핵심 기능이 다음과 같이 누락되었습니다.

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

WorkManager 작업 체인을 사용하여 위의 기능을 추가합니다.

WorkManager를 사용하면 순서대로 실행되거나 동시에 실행되는 별도의 WorkerRequest를 만들 수 있습니다. 이 단계에서는 다음과 같은 작업 체인을 만듭니다.

54832b34e9c9884a.png

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도 출력됩니다.

4fc29ac70fbecf85.png

이 Codelab은 파일 조작에 관해 다루지 않으므로, 아래의 코드를 사용하세요. KEY_IMAGE_URI 키로 resourceUrioutput 값이 어떻게 검색되는지 확인하세요. 입력 및 출력에 관한 최근 단계에서 작성한 코드와 매우 유사합니다(동일한 키를 모두 사용함).

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 체인을 실행하도록 BlurViewModelapplyBlur 메서드를 수정해야 합니다. 현재 코드는 다음과 같습니다.

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 체인이 생성되고 실행됩니다.

bf3b82eb9fd22349.png

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에서 블러 처리된 이미지를 확인할 수 있으며, 향후에 사용자가 기기에서 블러 처리된 이미지를 확인할 수 있도록 버튼을 추가할 예정입니다.

아래 스크린샷에서는 현재 실행 중인 작업자가 알림 메시지에 표시되는 것을 볼 수 있습니다.

f0bbaf643c24488f.png 42a036f4b24adddb.png

a438421064c385d4.png

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의 상태를 가져올 수 있습니다. WorkInfoWorkRequest의 현재 상태에 관한 다음과 같은 세부정보가 포함된 객체입니다.

다음 표에서는 LiveData<WorkInfo> 객체나 LiveData<List<WorkInfo>> 객체를 가져오는 세 가지 방법과 각 결과를 설명합니다.

유형

WorkManager 메서드

설명

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

getWorkInfoByIdLiveData

WorkRequest에는 WorkManager에서 생성된 고유 ID가 있습니다. 이 ID를 사용하여 바로
WorkRequest의 단일 LiveData를 얻을 수 있습니다.

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

getWorkInfosForUniqueWorkLiveData

방금 본 것처럼 WorkRequest는 고유 체인에 포함될 수 있습니다. 이 메서드는 고유한 단일 WorkRequests 체인에 있는 모든 작업의 LiveData
>
를 반환합니다.

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

getWorkInfosByTagLiveData

마지막으로, 선택적으로 WorkRequest를 String으로 태그 지정할 수 있습니다. 동일한 태그를 사용하여 여러 WorkRequest를 태그하여 연결할 수 있습니다. 이 메서드는 단일 태그의 LiveData
>
를 반환합니다.

SaveImageToFileWorker WorkRequest를 태그 지정하면 getWorkInfosByTag를 사용하여 가져올 수 있습니다. WorkManager ID를 사용하는 대신 태그를 사용하여 작업의 라벨을 지정하겠습니다. 왜냐하면 사용자가 여러 이미지를 블러 처리하는 경우 모든 이미지 저장 WorkRequest의 태그가 같지만 ID는 같지 않기 때문입니다. 또한 태그를 선택할 수도 있습니다.

getWorkInfosForUniqueWork를 사용하지 않습니다. 모든 블러 WorkRequest 및 정리 WorkRequestWorkInfo도 반환하기 때문입니다(이렇게 반환하려면 이미지 저장 WorkRequest를 찾기 위한 추가 로직이 필요함).

1단계 - 작업 태그 지정

applyBlur에서 SaveImageToFileWorker를 만들 때 String 상수 TAG_OUTPUT을 사용하여 작업에 태그를 지정합니다.

BlurViewModel.kt

val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
        .addTag(TAG_OUTPUT) // <-- ADD THIS
        .build()

2단계 - WorkInfo 가져오기

작업에 태그를 지정했으므로 이제 WorkInfo를 가져올 수 있습니다.

  1. BlurViewModel에서 outputWorkInfos라는 새 클래스 변수 LiveData<List<WorkInfo>>를 선언합니다.
  2. 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 표시

WorkInfoLiveData를 가져왔으며 이제 BlurActivity에서 관찰할 수 있습니다. 관찰자에서 다음을 따릅니다.

  1. WorkInfo 목록이 null이 아닌지, 그리고 목록에 WorkInfo 객체가 있는지 확인합니다. 없으면 적용 버튼을 아직 클릭하지 않은 것이므로 돌아갑니다.
  2. 목록의 첫 번째 WorkInfo를 가져옵니다. 작업 체인을 고유하게 만들었으므로 TAG_OUTPUT으로 태그 지정된 WorkInfo는 하나만 있습니다.
  3. workInfo.state.isFinished를 사용하여 작업이 완료 상태인지 확인합니다.
  4. 완료되지 않은 경우 showWorkInProgress()을 호출하여 진행 버튼을 숨기고 작업 취소 버튼과 진행률 표시줄을 표시합니다.
  5. 작업이 완료되면 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단계 - 앱 실행

앱을 실행합니다. 앱이 컴파일되고 실행되지만 이번에는 작동할 때 진행률 표시줄이 표시되고 취소 버튼도 표시됩니다.

7b70288f69050f0b.png

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 관찰자에 적용해야 하는 몇 가지 최종 조정이 있습니다.

  1. WorkInfo가 완료된 경우 workInfo.outputData를 사용하여 출력 데이터를 가져옵니다.
  2. 그런 다음 출력 URI를 가져옵니다. Constants.KEY_IMAGE_URI 키를 사용해 저장된다는 점을 기억하세요.
  3. 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단계 - 코드 실행

코드를 실행합니다. 클릭 가능한 새 파일 보기 버튼이 표시됩니다. 이 버튼을 클릭하면 출력 파일로 이동합니다.

5366222d0b4fb705.png

cd1ecc8b4ca86748.png

10. 작업 취소

bc1dc9414fe2326e.png

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단계 - 작업 실행 및 취소

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

dcb4ccfd261957b1.png

이제 WorkState가 더 이상 '완료됨' 상태가 아니므로 작업이 취소되면 '진행' 버튼만 표시됩니다.

11. 작업 제약 조건

마지막으로 WorkManagerConstraints를 지원합니다. 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 창에서 충전 상태를 변경할 수 있습니다.

406ce044ca07169f.png

기기가 충전 중이 아닐 때는 SaveImageToFileWorker,를 정지하고 기기를 연결한 후에만 실행해야 합니다.

302da5ec986ae769.png

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을 진행하세요.