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

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에 작업 상태 표시
  • 작업 취소
  • 작업 제약 조건

필요한 항목

언제든지 도움이 필요한 경우

이 Codelab을 사용하는 중에 언제든지 도움이 필요하거나 코드의 최종 상태를 확인하고 싶다면 다음 링크를 사용하면 됩니다.

최종 코드 다운로드

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

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

1단계 - 코드 다운로드

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

시작 코드 다운로드

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

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

2단계 - 이미지 가져오기

이미 이미지를 다운로드한 기기를 사용 중이거나 기기에서 사진을 찍었다면 모든 준비가 된 것입니다.

새 기기(예: 최근에 만든 에뮬레이터)를 사용하는 경우에는 기기를 사용하여 사진을 찍거나 웹에서 이미지를 다운로드하는 것이 좋습니다. 신비로운 이미지를 선택하세요.

3단계 - 앱 실행

앱을 실행합니다. 다음과 같은 화면이 표시됩니다. 이미지가 사용 중지되어 있으면 첫 프롬프트에서 사진 액세스 권한을 허용한 후에 앱을 다시 열어야 합니다.

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

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

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

  • WorkerUtils**:** 이 클래스에는 실제로 블러 처리를 하는 코드와 나중에 Notifications를 표시하고 앱 속도를 저하하는 데 사용하는 몇 가지 편의 메서드가 있습니다.
  • BlurActivity***:** 이미지를 표시하고 흐림 수준을 선택하는 라디오 버튼이 포함된 활동입니다.
  • BlurViewModel***:** 이 뷰 모델은 BlurActivity를 표시하는 데 필요한 데이터를 모두 저장합니다. WorkManager를 사용하여 백그라운드 작업을 시작하는 클래스이기도 합니다.
  • Constants**:** Codelab에서 사용할 상수가 포함된 정적 클래스입니다.
  • SelectImageActivity**:** 개발자가 이미지를 선택할 수 있는 첫 번째 활동입니다.
  • res/activity_blur.xmlres/activity_select.xml: 각 활동의 레이아웃 파일입니다.

***** 이 파일에만 코드를 작성합니다.

WorkManager에는 아래의 Gradle 종속 항목이 필요합니다. 빌드 파일에 이미 포함되어 있는 항목입니다.

app/build.gradle

dependencies {
    // Other dependencies
    implementation "androidx.work:work-runtime:$versions.work"
}

여기에서 최신 버전의 work-runtime을 가져와 올바른 버전을 입력하세요. 현재 최신 버전은 다음과 같습니다.

build.gradle

versions.work = "2.3.3"

버전을 최신 버전으로 업데이트하는 경우 Sync Now를 통해 프로젝트를 변경된 gradle 파일과 동기화해야 합니다.

이 단계에서는 res/drawable 폴더의 test.jpg라는 이미지에 몇 가지 함수를 백그라운드에서 실행합니다. 이러한 함수는 이미지를 블러 처리한 후 임시 파일에 저장합니다.

WorkManager 기본사항

알아야 할 몇 가지 WorkManager 클래스가 있습니다.

  • Worker: 백그라운드에서 실행하고자 하는 실제 작업의 코드를 여기에 입력합니다. 이 클래스를 확장하고 doWork() 메서드를 재정의합니다.
  • WorkRequest: 작업 실행 요청을 나타냅니다. WorkRequest를 만드는 과정에서 Worker를 전달합니다. WorkRequest를 만들 때 Worker를 실행할 시점에 적용되는 Constraints 등을 지정할 수도 있습니다.
  • WorkManager: 이 클래스는 실제로 WorkRequest를 예약하고 실행합니다. 지정된 제약 조건을 준수하면서 시스템 리소스에 부하를 분산하는 방식으로 WorkRequest를 예약합니다.

여기에서는 이미지를 블러 처리하는 코드를 포함하는 새 BlurWorker를 정의합니다. 적용 버튼을 클릭하면 WorkRequest가 생성된 다음 WorkManager에 의해 큐에 추가됩니다.

1단계 - BlurWorker 만들기

workers 패키지에서 BlurWorker라는 새 클래스를 만듭니다.

이 클래스는 Worker를 확장합니다.

2단계 - 생성자 추가

BlurWorker 클래스에 생성자를 추가합니다.

public class BlurWorker extends Worker {
    public BlurWorker(
        @NonNull Context appContext,
        @NonNull WorkerParameters workerParams) {
            super(appContext, workerParams);
    }
}

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

Workerres/test.jpg 이미지를 블러 처리합니다.

doWork() 메서드를 재정의하고 다음을 구현합니다.

  1. getApplicationContext()를 호출하여 Context를 가져옵니다. 처리할 다양한 비트맵 조작을 위해 필요합니다.
  2. 테스트 이미지에서 Bitmap을 만듭니다.
Bitmap picture = BitmapFactory.decodeResource(
    applicationContext.getResources(),
    R.drawable.test);
  1. WorkerUtils에서 정적 blurBitmap 메서드를 호출하여 블러 처리된 버전의 비트맵을 가져옵니다.
  2. WorkerUtils에서 정적 writeBitmapToFile 메서드를 호출하여 이 비트맵을 임시 파일에 씁니다. 반환된 URI를 로컬 변수에 저장해야 합니다.
  3. WorkerUtils에서 정적 makeStatusNotification 메서드를 호출하여 URI를 표시하는 알림을 만듭니다.
  4. Result.success();를 반환합니다.
  5. 2~6단계의 코드를 try/catch 문으로 래핑합니다. 일반 Throwable을 포착합니다.
  6. catch 문에서 다음 오류 로그 구문을 내보냅니다. Log.e(TAG, "Error applying blur", throwable);
  7. catch 문에서 Result.failure();를 반환합니다.

이 단계에서 완성된 코드는 아래와 같습니다.

BlurWorker.java

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Log;

import com.example.background.R;

import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class BlurWorker extends Worker {
    public BlurWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }

    private static final String TAG = BlurWorker.class.getSimpleName();

    @NonNull
    @Override
    public Result doWork() {

        Context applicationContext = getApplicationContext();

        try {

            Bitmap picture = BitmapFactory.decodeResource(
                    applicationContext.getResources(),
                    R.drawable.test);

            // Blur the bitmap
            Bitmap output = WorkerUtils.blurBitmap(picture, applicationContext);

            // Write bitmap to a temp file
            Uri outputUri = WorkerUtils.writeBitmapToFile(applicationContext, output);

            WorkerUtils.makeStatusNotification("Output is "
                    + outputUri.toString(), applicationContext);

            // If there were no errors, return SUCCESS
            return Result.success();
        } catch (Throwable throwable) {

            // Technically WorkManager will return Result.failure()
            // but it's best to be explicit about it.
            // Thus if there were errors, we're return FAILURE
            Log.e(TAG, "Error applying blur", throwable);
            return Result.failure();
        }
    }
}

4단계 - ViewModel에서 WorkManager 가져오기

ViewModel에서 WorkManager 인스턴스의 변수를 만들고 ViewModel의 생성자에서 인스턴스화합니다.

BlurViewModel.java

private WorkManager mWorkManager;

// BlurViewModel constructor
public BlurViewModel(@NonNull Application application) {
  super(application);
  mWorkManager = WorkManager.getInstance(application);

  //...rest of the constructor
}

5단계 - WorkManager에서 WorkRequest를 큐에 추가

이제 WorkRequest를 만들고 WorkManager에 실행하도록 지시합니다. 두 가지 WorkRequest 유형이 있습니다.

  • OneTimeWorkRequest: 한 번만 실행되는 WorkRequest입니다.
  • PeriodicWorkRequest: 일정 주기로 반복할 WorkRequest입니다.

적용 버튼을 클릭할 때 한 번만 이미지를 블러 처리하도록 설정하고 싶습니다. 적용 버튼을 클릭하면 applyBlur 메서드가 호출되므로 이 메서드의 BlurWorker에서 OneTimeWorkRequest를 만듭니다. 그런 다음 WorkManager 인스턴스를 사용하여 WorkRequest.를 큐에 추가합니다.

BlurViewModel의 applyBlur() 메서드에 다음 코드 줄을 추가합니다.

BlurViewModel.java

void applyBlur(int blurLevel) {
   mWorkManager.enqueue(OneTimeWorkRequest.from(BlurWorker.class));
}

6단계 - 코드 실행

코드를 실행합니다. 컴파일되고 적용 버튼을 누르면 알림이 표시됩니다.

7ef0320960f4d756.png

선택적으로, Android 스튜디오에서 Device File Explorer를 열 수 있습니다.

cf10a1af6e84f5ff.png

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

7f5eba3559b44cbb.png

테스트 이미지의 블러 처리는 잘 되었습니다. 하지만 Blur-O-Matic이 진정으로 혁신적인 이미지 편집 앱이 되기 위해서는 사용자에게 직접 이미지를 블러 처리하도록 해야 합니다.

이를 위해 사용자가 선택한 이미지의 URI를 WorkRequest입력으로 제공합니다.

1단계 - Data 입력 객체 만들기

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

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

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

  1. Data.Builder 객체를 만듭니다.
  2. mImageUri가 null이 아닌 URI이면 putString 메서드를 사용하여 Data 객체에 추가합니다. 이 메서드는 키와 값을 사용합니다. Constants 클래스의 문자열 상수 KEY_IMAGE_URI를 사용할 수 있습니다.
  3. Data.Builder 객체에서 build()를 호출하여 Data 객체를 만들고 반환합니다.

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

BlurViewModel.java

/**
 * Creates the input data bundle which includes the Uri to operate on
 * @return Data which contains the Image Uri as a String
 */
private Data createInputDataForUri() {
    Data.Builder builder = new Data.Builder();
    if (mImageUri != null) {
        builder.putString(KEY_IMAGE_URI, mImageUri.toString());
    }
    return builder.build();
}

2단계 - WorkRequest에 Data 객체 전달

다음을 처리하도록 applyBlur 메서드를 변경합니다.

  1. OneTimeWorkRequest.Builder를 만듭니다.
  2. setInputData를 호출하여 createInputDataForUri의 결과를 전달합니다.
  3. OneTimeWorkRequest를 빌드합니다.
  4. WorkManager를 사용하여 요청을 큐에 추가합니다.

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

BlurViewModel.java

void applyBlur(int blurLevel) {
   OneTimeWorkRequest blurRequest =
                new OneTimeWorkRequest.Builder(BlurWorker.class)
                        .setInputData(createInputDataForUri())
                        .build();

   mWorkManager.enqueue(blurRequest);
}

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

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

BlurWorker.java

public Result doWork() {

       Context applicationContext = getApplicationContext();

        // ADD THIS LINE
       String resourceUri = getInputData().getString(Constants.KEY_IMAGE_URI);

        //... rest of doWork()
}

이 변수는 다음 단계를 완료할 때까지 사용되지 않습니다.

4단계 - 지정된 URI 블러 처리

URI에서 사용자가 선택한 이미지를 블러 처리할 수 있습니다.

BlurWorker.java

public Worker.Result doWork() {
       Context applicationContext = getApplicationContext();

       String resourceUri = getInputData().getString(Constants.KEY_IMAGE_URI);

    try {

        // REPLACE THIS CODE:
        // Bitmap picture = BitmapFactory.decodeResource(
        //        applicationContext.getResources(),
        //        R.drawable.test);
        // WITH
        if (TextUtils.isEmpty(resourceUri)) {
            Log.e(TAG, "Invalid input uri");
            throw new IllegalArgumentException("Invalid input uri");
        }

        ContentResolver resolver = applicationContext.getContentResolver();
        // Create a bitmap
        Bitmap picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)));
        //...rest of doWork

5단계 - 임시 URI 출력

이 Worker를 완료했으며 이제 Result.success()를 반환할 수 있습니다. OutputURI를 출력 데이터로 제공하여 추가 작업을 위해 다른 Worker가가 이 임시 이미지에 쉽게 액세스할 수 있도록 합니다. 이렇게 하면 다음 장에서 Worker 체인을 만들 때 유용합니다. 방법은 다음과 같습니다.

  1. Data를 만들고 입력의 경우와 마찬가지로 outputUriString으로 저장합니다. 같은 키(KEY_IMAGE_URI)를 사용합니다.
  2. WorkerResult.success() 메서드에 전달합니다.

BlurWorker.java

다음 줄을 WorkerUtils.makeStatusNotification 줄 다음에 배치하여 doWork()Result.success()를 대체합니다.

Data outputData = new Data.Builder()
    .putString(KEY_IMAGE_URI, outputUri.toString())
    .build();
return Result.success(outputData);

6단계 - 앱 실행

이 시점에서 앱을 실행해야 합니다. 앱이 컴파일되고 똑같이 동작해야 합니다.

최근 단계에서 한 것처럼 선택적으로, Android 스튜디오에서 Device File Explorer를 열고 data/data/com.example.background/files/blur_filter_outputs/<URI>로 이동합니다.

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

7e717ffd6b3d9d52.png

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

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

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

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

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

54832b34e9c9884a.png

WorkRequest는 상자로 표시되어 있습니다.

체이닝을 위한 또 다른 멋진 기능은 한 WorkRequest의 출력이 체인 내 다음 WorkRequest의 입력이 된다는 점입니다. 각 WorkRequest 간에 전달되는 입력과 출력은 파란색 텍스트로 표시되어 있습니다.

1단계 - 정리 Worker와 저장 Worker 만들기

먼저 필요한 Worker 클래스를 모두 정의합니다. 이미지를 블러 처리하는 Worker는 이미 있지만 임시 파일을 정리하는 Worker와 이미지를 영구적으로 저장하는 Worker도 필요합니다.

worker 패키지에 Worker를 확장하는 새 클래스 두 개를 만듭니다.

하나는 CleanupWorker로, 다른 하나는 SaveImageToFileWorker로 지정해야 합니다.

2단계 - 생성자 추가

CleanupWorker 클래스에 생성자를 추가합니다.

public class CleanupWorker extends Worker {
    public CleanupWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }
}

3단계 - doWork()를 재정의하여 CleanupWorker용으로 구현

CleanupWorker는 입력을 받거나 출력을 전달할 필요가 없습니다. 임시 파일이 있으면 항상 삭제합니다. 이 Codelab은 파일 조작에 관해 다루지 않으므로, 아래에서 CleanupWorker 관련 코드를 복사하면 됩니다.

CleanupWorker.java

import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.example.background.Constants;
import java.io.File;

public class CleanupWorker extends Worker {
    public CleanupWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }

    private static final String TAG = CleanupWorker.class.getSimpleName();

    @NonNull
    @Override
    public Result doWork() {
        Context applicationContext = getApplicationContext();

        // 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
        WorkerUtils.makeStatusNotification("Cleaning up old temporary files",
                applicationContext);
        WorkerUtils.sleep();

        try {
            File outputDirectory = new File(applicationContext.getFilesDir(),
                    Constants.OUTPUT_PATH);
            if (outputDirectory.exists()) {
                File[] entries = outputDirectory.listFiles();
                if (entries != null && entries.length > 0) {
                    for (File entry : entries) {
                        String name = entry.getName();
                        if (!TextUtils.isEmpty(name) && name.endsWith(".png")) {
                            boolean deleted = entry.delete();
                            Log.i(TAG, String.format("Deleted %s - %s",
                                    name, deleted));
                        }
                    }
                }
            }

            return Worker.Result.success();
        } catch (Exception exception) {
            Log.e(TAG, "Error cleaning up", exception);
            return Worker.Result.failure();
        }
    }
}

4단계 - doWork()를 재정의하여 SaveImageToFileWorker용으로 구현

SaveImageToFileWorker는 입력과 출력을 처리합니다. 입력은 KEY_IMAGE_URI 키로 저장된 String입니다. 출력 또한 KEY_IMAGE_URI 키로 저장된 String입니다.

475a08a82ea675ca.png

이 Codelab은 파일 조작에 관해 다루지 않으므로, 아래의 코드를 사용하면 됩니다. 코드에 TODO 두 개가 포함되어 있으며, 여기에 입력과 출력에 적합한 코드를 직접 입력하세요. 입력 및 출력에 관한 최근 단계에서 작성한 코드와 매우 유사합니다(동일한 키를 모두 사용함).

SaveImageToFileWorker.java

import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.example.background.Constants;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class SaveImageToFileWorker extends Worker {
    public SaveImageToFileWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }

    private static final String TAG = SaveImageToFileWorker.class.getSimpleName();

    private static final String TITLE = "Blurred Image";
    private static final SimpleDateFormat DATE_FORMATTER =
            new SimpleDateFormat("yyyy.MM.dd 'at' HH:mm:ss z", Locale.getDefault());

    @NonNull
    @Override
    public Result doWork() {
        Context applicationContext = getApplicationContext();

        // 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
        WorkerUtils.makeStatusNotification("Saving image", applicationContext);
        WorkerUtils.sleep();

        ContentResolver resolver = applicationContext.getContentResolver();
        try {
            String resourceUri = getInputData()
                    .getString(Constants.KEY_IMAGE_URI);
            Bitmap bitmap = BitmapFactory.decodeStream(
                    resolver.openInputStream(Uri.parse(resourceUri)));
            String outputUri = MediaStore.Images.Media.insertImage(
                    resolver, bitmap, TITLE, DATE_FORMATTER.format(new Date()));
            if (TextUtils.isEmpty(outputUri)) {
                Log.e(TAG, "Writing to MediaStore failed");
                return Result.failure();
            }
            Data outputData = new Data.Builder()
                    .putString(Constants.KEY_IMAGE_URI, outputUri)
                    .build();
            return Result.success(outputData);
        } catch (Exception exception) {
            Log.e(TAG, "Unable to save image to Gallery", exception);
            return Worker.Result.failure();
        }
    }
}

5단계 - BlurWorker 알림 수정

올바른 폴더에 이미지를 저장하는 Worker 체인을 만들었으므로 이제 에뮬레이션된 기기에서도 각 WorkRequest의 시작을 더 쉽게 보도록 하기 위해 작업 시작 및 작업 속도 저하 시 사용자에게 전달하도록 알림을 수정할 수 있습니다. BlurWorker의 최종 버전은 다음과 같습니다.

BlurWorker.java

@NonNull
@Override
public Worker.Result doWork() {

    Context applicationContext = getApplicationContext();

    // 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
    WorkerUtils.makeStatusNotification("Blurring image", applicationContext);
    WorkerUtils.sleep();
    String resourceUri = getInputData().getString(KEY_IMAGE_URI);

    try {

        if (TextUtils.isEmpty(resourceUri)) {
            Log.e(TAG, "Invalid input uri");
            throw new IllegalArgumentException("Invalid input uri");
        }

        ContentResolver resolver = applicationContext.getContentResolver();
        // Create a bitmap
        Bitmap picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)));

        // Blur the bitmap
        Bitmap output = WorkerUtils.blurBitmap(picture, applicationContext);

        // Write bitmap to a temp file
        Uri outputUri = WorkerUtils.writeBitmapToFile(applicationContext, output);

        Data outputData = new Data.Builder()
                .putString(KEY_IMAGE_URI, outputUri.toString())
                .build();

        // If there were no errors, return SUCCESS
        return Result.success(outputData);
    } catch (Throwable throwable) {

        // Technically WorkManager will return Result.failure()
        // but it's best to be explicit about it.
        // Thus if there were errors, we're return FAILURE
        Log.e(TAG, "Error applying blur", throwable);
        return Result.failure();
    }
}

6단계 - WorkRequest 체인 만들기

하나만 실행하는 것이 아니라 WorkRequest 체인을 실행하도록 BlurViewModelapplyBlur 메서드를 수정해야 합니다. 현재 코드는 다음과 같습니다.

BlurViewModel.java

void applyBlur(int blurLevel) {
    OneTimeWorkRequest blurRequest =
            new OneTimeWorkRequest.Builder(BlurWorker.class)
                    .setInputData(createInputDataForUri())
                    .build();

    mWorkManager.enqueue(blurRequest);
}

WorkManager.enqueue()를 호출하는 대신 WorkManager.beginWith()를 호출합니다. 그러면 WorkRequest 체인을 정의하는 WorkContinuation이 반환됩니다. then() 메서드를 호출하여 이 작업 요청 체인에 추가할 수 있습니다. 예를 들어 WorkRequest 객체 세 개(workA, workB, workC)가 있는 경우 다음과 같이 합니다.

// Example code. Don't copy to the project
WorkContinuation continuation = mWorkManager.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 체인이 생성되고 실행됩니다.

2c4bf31e5f6522ad.png

applyBlur에서 CleanupWorker WorkRequest, BlurImage WorkRequest, SaveImageToFile WorkRequest의 체인을 만듭니다. BlurImage WorkRequest에 입력을 전달합니다.

이를 위한 코드는 다음과 같습니다.

BlurViewModel.java

void applyBlur(int blurLevel) {

    // Add WorkRequest to Cleanup temporary images
    WorkContinuation continuation =
        mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));

    // Add WorkRequest to blur the image
    OneTimeWorkRequest blurRequest = new OneTimeWorkRequest.Builder(BlurWorker.class)
                    .setInputData(createInputDataForUri())
                    .build();
    continuation = continuation.then(blurRequest);

    // Add WorkRequest to save the image to the filesystem
    OneTimeWorkRequest save =
        new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
            .build();
    continuation = continuation.then(save);

    // Actually start the work
    continuation.enqueue();
}

그러면 컴파일되고 실행됩니다. 블러 처리하도록 선택한 이미지가 이제 사진 폴더에 저장된 것을 확인할 수 있습니다.

e2d29f34bdf01860.png

7단계 - BlurWorker 반복

서로 다른 양으로 이미지를 블러 처리하는 기능을 추가합니다. applyBlur에 전달된 blurLevel 매개변수를 사용하여 매개변수에 지정된 수량의 WorkRequest 작업을 체인에 추가합니다. 첫 번째 WorkRequest만 URI가 필요하며 URI 입력을 받아야 합니다.

직접 해 보고 아래의 코드와 비교하세요.

BlurViewModel.java

void applyBlur(int blurLevel) {

    // Add WorkRequest to Cleanup temporary images
    WorkContinuation continuation = mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));

    // Add WorkRequests to blur the image the number of times requested
    for (int i = 0; i < blurLevel; i++) {
        OneTimeWorkRequest.Builder blurBuilder =
                new OneTimeWorkRequest.Builder(BlurWorker.class);

        // 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
    OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
            .build();
    continuation = continuation.then(save);

    // Actually start the work
    continuation.enqueue();
}

잘하셨습니다. 이제 원하는 양만큼 이미지를 블러 처리할 수 있습니다. 정말 유용하죠.

fcb326118dd99959.png

체인을 사용해 봤습니다. 이제 WorkManager의 또 다른 강력한 기능인 고유 작업 체인을 알아보겠습니다.

작업 체인을 한 번에 하나씩만 실행해야 하는 경우가 있습니다. 예를 들어 로컬 데이터를 서버와 동기화하는 작업 체인이 있는 경우, 첫 번째 데이터 동기화가 완료된 후에 새 동기화가 시작되도록 할 수 있습니다. 이렇게 하려면 beginWith 대신 beginUniqueWork를 사용하고 고유한 String 이름을 제공합니다. 함께 참조하고 쿼리할 수 있도록 전체 작업 요청 체인을 지정합니다.

파일을 블러 처리하는 작업 체인이 고유하도록 beginUniqueWork를 사용합니다. 키로 IMAGE_MANIPULATION_WORK_NAME을 전달합니다. ExistingWorkPolicy도 전달해야 합니다. 사용할 수 있는 옵션은 REPLACE, KEEP, APPEND입니다.

사용자가 현재 이미지가 완료되기 전에 다른 이미지를 블러 처리하려는 경우 현재 이미지가 중지되고 새 이미지가 블러 처리되도록 지정할 계획이므로 REPLACE를 사용합니다.

고유 작업 연속 처리를 시작하는 코드는 다음과 같습니다.

BlurViewModel.java

// REPLACE THIS CODE:
// WorkContinuation continuation =
// mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));
// WITH
WorkContinuation continuation = mWorkManager
                .beginUniqueWork(IMAGE_MANIPULATION_WORK_NAME,
                       ExistingWorkPolicy.REPLACE,
                       OneTimeWorkRequest.from(CleanupWorker.class));

이제 Blur-O-Matic은 한 번에 사진 한 장만 블러 처리합니다.

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

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

1단계 - 작업 태그 지정

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

BlurViewModel.java

OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
        .addTag(TAG_OUTPUT) // This adds the tag
        .build();

2단계 - WorkInfo 가져오기

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

  1. LiveData<List<WorkInfo>>mSavedWorkInfo라는 새 변수를 선언합니다.
  2. BlurViewModel 생성자에서 WorkManager.getWorkInfosByTagLiveData를 사용하여 WorkInfo를 가져옵니다.
  3. mSavedWorkInfo getter 추가

필요한 코드는 다음과 같습니다.

BlurViewModel.java

// New instance variable for the WorkInfo class
private LiveData<List<WorkInfo>> mSavedWorkInfo;

// Placed this code in the BlurViewModel constructor
mSavedWorkInfo = mWorkManager.getWorkInfosByTagLiveData(TAG_OUTPUT);

// Add a getter method for mSavedWorkInfo
LiveData<List<WorkInfo>> getOutputWorkInfo() { return mSavedWorkInfo; }

3단계 - WorkInfo 표시

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

  1. WorkInfo 목록이 null이 아닌지, 그리고 목록에 WorkInfo 객체가 있는지 확인합니다. 없으면 적용 버튼을 아직 클릭하지 않은 것이므로 돌아갑니다.
  2. 목록의 첫 번째 WorkInfo를 가져옵니다. 작업 체인을 고유하게 만들었으므로 TAG_OUTPUT으로 태그 지정된 WorkInfo는 하나만 있습니다.
  3. workInfo.getState().isFinished();를 사용하여 작업이 완료 상태인지 확인합니다.
  4. 완료되지 않은 경우 showWorkInProgress()를 호출하여 적절한 뷰를 숨기고 표시합니다.
  5. 완료된 경우 showWorkFinished()를 호출하여 적절한 뷰를 숨기고 표시합니다.

코드는 다음과 같습니다.

BlurActivity.java

// Show work status, added in onCreate()
mViewModel.getOutputWorkInfo().observe(this, listOfWorkInfos -> {

    // If there are no matching work info, do nothing
    if (listOfWorkInfos == null || listOfWorkInfos.isEmpty()) {
        return;
    }

    // We only care about the first output status.
    // Every continuation has only one worker tagged TAG_OUTPUT
    WorkInfo workInfo = listOfWorkInfos.get(0);

    boolean finished = workInfo.getState().isFinished();
    if (!finished) {
        showWorkInProgress();
    } else {
        showWorkFinished();
    }
});

4단계 - 앱 실행

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

b7d8d3182f91ce23.png

WorkInfo에는 저장된 최종 이미지가 있는 출력 Data 객체를 가져올 수 있는 getOutputData 메서드도 있습니다. 표시할 준비가 된 블러 처리된 이미지가 있으면 항상 See File이라는 버튼을 표시해 보겠습니다.

1단계 - mOutputUri 만들기

BlurViewModel에서 최종 URI의 변수를 만들고 이 변수의 getter와 setter를 제공합니다. StringUri로 변환하려면 uriOrNull 메서드를 사용하면 됩니다.

다음 코드를 사용할 수 있습니다.

BlurViewModel.java

// New instance variable for the WorkInfo
private Uri mOutputUri;

// Add a getter and setter for mOutputUri
void setOutputUri(String outputImageUri) {
    mOutputUri = uriOrNull(outputImageUri);
}

Uri getOutputUri() { return mOutputUri; }

2단계 - See File 버튼 만들기

activity_blur.xml 레이아웃에 숨겨진 버튼이 이미 있습니다. BlurActivity에 있으며 뷰 바인딩을 통해 seeFileButton으로 액세스할 수 있습니다.

이 버튼의 클릭 리스너를 설정합니다. 리스너는 URI를 가져와서 이 URI를 보는 활동을 열어야 합니다. 다음 코드를 사용할 수 있습니다.

BlurActivity.java

// Inside onCreate()

binding.seeFileButton.setOnClickListener(view -> {
    Uri currentUri = mViewModel.getOutputUri();
    if (currentUri != null) {
        Intent actionView = new Intent(Intent.ACTION_VIEW, currentUri);
        if (actionView.resolveActivity(getPackageManager()) != null) {
            startActivity(actionView);
        }
    }
});

3단계 - URI 설정 및 버튼 표시

작동하기 위해 WorkInfo 관찰자에 적용해야 하는 몇 가지 최종 조정이 있습니다.

  1. WorkInfo가 완료된 경우 workInfo.getOutputData().를 사용하여 출력 데이터를 가져옵니다.
  2. 그런 다음 출력 URI를 가져옵니다. Constants.KEY_IMAGE_URI 키를 사용해 저장된다는 점을 기억하세요.
  3. URI가 비어 있지 않으면 올바르게 저장된 것입니다. seeFileButton을 표시하고 URI를 사용하여 뷰 모델에서 setOutputUri를 호출합니다.

BlurActivity.java

// Replace the observer code we added in previous steps with this one.
// Show work info, goes inside onCreate()
mViewModel.getOutputWorkInfo().observe(this, listOfWorkInfo -> {

    // If there are no matching work info, do nothing
    if (listOfWorkInfo == null || listOfWorkInfo.isEmpty()) {
        return;
    }

    // We only care about the first output status.
    // Every continuation has only one worker tagged TAG_OUTPUT
    WorkInfo workInfo = listOfWorkInfo.get(0);

    boolean finished = workInfo.getState().isFinished();
    if (!finished) {
        showWorkInProgress();
    } else {
        showWorkFinished();
        Data outputData = workInfo.getOutputData();

        String outputImageUri = outputData.getString(Constants.KEY_IMAGE_URI);

        // If there is an output file show "See File" button
        if (!TextUtils.isEmpty(outputImageUri)) {
            mViewModel.setOutputUri(outputImageUri);
            binding.seeFileButton.setVisibility(View.VISIBLE);
        }
    }
});

4단계 - 코드 실행

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

992d0b2390600774.png

bc1dc9414fe2326e.png

Cancel Work 버튼을 추가했으므로 이 버튼이 동작하도록 코드를 추가합니다. WorkManager를 사용하면 ID, 태그, 고유 체인 이름을 사용하여 작업을 취소할 수 있습니다.

여기서는 특정 단계뿐 아니라 체인의 모든 작업을 취소하려고 하므로 고유한 체인 이름으로 작업을 취소하는 것이 좋습니다.

1단계 - 이름으로 작업 취소

뷰 모델에서 작업을 취소하는 메서드를 작성합니다.

BlurViewModel.java

/**
 * Cancel work using the work's unique name
 */
void cancelWork() {
    mWorkManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME);
}

2단계 - 취소 메서드 호출

cancelWork를 호출하는 cancelButton 버튼을 연결합니다.

BlurActivity.java

// In onCreate()

// Hookup the Cancel button
binding.cancelButton.setOnClickListener(view -> mViewModel.cancelWork());

3단계 - 작업 실행 및 취소

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

bdaadc9bb25472cb.png

또한, WorkManagerConstraints를 지원합니다. Blur-O-Matic의 경우 저장 시 기기를 충전해야 한다는 제약 조건을 사용합니다.

1단계 - 충전 제약 조건 만들기 및 추가

Constraints 객체를 만들려면 Constraints.Builder를 사용합니다. 그런 다음 아래와 같이 원하는 제약 조건을 설정하고 WorkRequest에 추가합니다.

BlurViewModel.java

// In the applyBlur method

// Create charging constraint
Constraints constraints = new Constraints.Builder()
        .setRequiresCharging(true)
        .build();

// Add WorkRequest to save the image to the filesystem
OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
        .setConstraints(constraints) // This adds the Constraints
        .addTag(TAG_OUTPUT)
        .build();

continuation = continuation.then(save);

2단계 - 에뮬레이터나 기기로 테스트

이제 Blur-O-Matic을 실행할 수 있습니다. 기기를 사용하는 경우 기기를 삭제하거나 연결할 수 있습니다. 에뮬레이터를 사용하는 경우 Extended controls 창에서 충전 상태를 변경할 수 있습니다.

c2e56295cbe73f8.png

기기가 충전 중이 아닐 때는 기기를 연결할 때까지 계속 로드 중 상태여야 합니다.

b7d8d3182f91ce23.png

축하합니다. Blur-O-Matic 앱을 완료했으며 그 과정에서 다음을 배웠습니다.

  • 프로젝트에 WorkManager 추가
  • OneOffWorkRequest 예약
  • 입력 및 출력 매개변수
  • 작업 WorkRequest 체이닝
  • 고유 WorkRequest 체인 이름 지정
  • WorkRequest 태그 지정
  • UI에 WorkInfo 표시
  • WorkRequest 취소
  • WorkRequest에 제약 조건 추가

좋습니다. 코드의 최종 상태와 모든 변경사항을 보려면 다음을 확인하세요.

최종 코드 다운로드

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

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

WorkManager는 반복 작업, 테스트 지원 라이브러리, 병렬 작업 요청, 병합 입력을 비롯하여 이 Codelab에서 다룰 수 있는 것보다 훨씬 더 많은 것을 지원합니다. 자세한 내용은 WorkManager 문서를 참고하세요.