Android에는 지연 가능한 백그라운드 작업을 위한 다양한 옵션이 있습니다. 이 Codelab에서는 WorkManager에 관해 알아봅니다. WorkManager는 유연하고 간편하며 호환성 있는 라이브러리로, 지연 가능한 백그라운드 작업을 지원합니다. WorkManager는 Android에서 권장되는 작업 스케줄러로, 지연 가능한 작업을 실행하도록 보장합니다.
WorkManager란?
WorkManager는 상황별 실행과 보장된 실행을 조합하여 적용해야 하는 백그라운드 작업을 위한 아키텍처 구성요소로서 Android Jetpack의 일부입니다. 상황별 실행을 적용하면 WorkManager가 최대한 빨리 백그라운드 작업을 실행합니다. 보장된 실행을 적용하면 WorkManager가 사용자가 앱을 벗어난 경우를 비롯한 다양한 상황에서 로직을 처리하여 작업을 시작합니다.
WorkManager는 단순하면서도 매우 유연한 라이브러리로, 이외에도 다음과 같은 다양한 이점이 있습니다.
- 비동기 일회성 작업과 주기적인 작업 모두 지원
- 네트워크 상태, 저장공간, 충전 상태와 같은 제약 조건 지원
- 동시 작업 실행을 포함한 복잡한 작업 요청 체이닝
- 한 작업 요청의 출력이 다음 작업 요청의 입력으로 사용됨
- 하위 버전인 API 수준 14와 호환성 처리(참고 확인)
- Google Play 서비스 사용 여부와 관계없이 작동함
- 시스템 상태 권장사항 준수
- UI에 작업 요청 상태를 쉽게 표시하는 LiveData 지원
WorkManager가 적합한 작업
WorkManager 라이브러리는 사용자가 특정 화면이나 앱에서 나가더라도 완료하는 것이 좋은 작업에 적합합니다.
WorkManager는 아래와 같은 작업에 사용하는 것이 적합합니다.
- 로그 업로드
- 이미지에 필터 적용 및 이미지 저장
- 주기적으로 로컬 데이터를 네트워크와 동기화
WorkManager는 보장된 실행을 제공하지만, 모든 작업에 보장된 실행이 필요하지는 않습니다. 따라서 기본 스레드에서 모든 작업을 실행하기 위한 포괄적인 기능은 아닙니다. 어떤 작업에 WorkManager를 사용할지 자세히 알아보려면 백그라운드 처리 가이드를 참고하세요.
빌드할 기능
요즘은 스마트폰이 사진을 정말 잘 찍습니다. 신비로운 대상을 사진가가 안정적으로 흐리게 처리한 사진으로 찍는 시대는 이제 지났습니다.
이 Codelab에서는 사진과 이미지를 블러 처리하여 결과를 파일에 저장하는 앱인 Blur-O-Matic을 작업합니다. 네스 호의 괴물인지 evelopera 장난감 잠수함인지 궁금하게 만드는 사진을 Blur-O-Matic을 통해 만들 수 있습니다.
잡종 줄무늬 농어(사진: 페기 그레브), 미국 농무부 연구소 |
학습할 내용
- 프로젝트에 WorkManager 추가
- 단순한 작업 예약
- 입력 및 출력 매개변수
- 작업 체이닝
- 고유 작업
- UI에 작업 상태 표시
- 작업 취소
- 작업 제약 조건
필요한 항목
- Android 스튜디오 최신 공개 버전이 있어야 합니다.
LiveData
및ViewModel
을 잘 알고 있어야 합니다. 이러한 클래스를 처음 사용하는 경우 Android 수명 주기 인식 구성요소 Codelab(ViewModel 및 LiveData 전용) 또는 뷰를 사용한 Room Codelab(아키텍처 구성요소 소개)을 참고하세요.
언제든지 도움이 필요한 경우
이 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.xml
및res/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() 재정의 및 구현
Worker
가 res/test.jpg
이미지를 블러 처리합니다.
doWork()
메서드를 재정의하고 다음을 구현합니다.
getApplicationContext()
를 호출하여Context
를 가져옵니다. 처리할 다양한 비트맵 조작을 위해 필요합니다.- 테스트 이미지에서
Bitmap
을 만듭니다.
Bitmap picture = BitmapFactory.decodeResource(
applicationContext.getResources(),
R.drawable.test);
WorkerUtils
에서 정적blurBitmap
메서드를 호출하여 블러 처리된 버전의 비트맵을 가져옵니다.WorkerUtils
에서 정적writeBitmapToFile
메서드를 호출하여 이 비트맵을 임시 파일에 씁니다. 반환된 URI를 로컬 변수에 저장해야 합니다.WorkerUtils
에서 정적makeStatusNotification
메서드를 호출하여 URI를 표시하는 알림을 만듭니다.Result.success();
를 반환합니다.- 2~6단계의 코드를 try/catch 문으로 래핑합니다. 일반
Throwable
을 포착합니다. - catch 문에서 다음 오류 로그 구문을 내보냅니다.
Log.e(TAG, "Error applying blur", throwable);
- 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단계 - 코드 실행
코드를 실행합니다. 컴파일되고 적용 버튼을 누르면 알림이 표시됩니다.
선택적으로, Android 스튜디오에서 Device File Explorer를 열 수 있습니다.
그런 다음 data>data>com.example.background>files>blur_filter_outputs><URI>로 이동하여 물고기가 실제로 블러 처리되었는지 확인합니다.
테스트 이미지의 블러 처리는 잘 되었습니다. 하지만 Blur-O-Matic이 진정으로 혁신적인 이미지 편집 앱이 되기 위해서는 사용자에게 직접 이미지를 블러 처리하도록 해야 합니다.
이를 위해 사용자가 선택한 이미지의 URI를 WorkRequest
의 입력으로 제공합니다.
1단계 - Data 입력 객체 만들기
입력 및 출력은 Data
객체를 통해 안팎으로 전달됩니다. Data
객체는 키-값 쌍의 경량 컨테이너입니다. WorkRequest
의 안팎으로 전달될 수 있는 소량의 데이터를 저장하기 위한 것입니다.
사용자 이미지의 URI를 번들로 전달할 것입니다. 이 URI는 mImageUri
라는 변수에 저장됩니다.
createInputDataForUri
라는 비공개 메서드를 만듭니다. 이 메서드는 다음과 같은 역할을 합니다.
Data.Builder
객체를 만듭니다.mImageUri
가 null이 아닌URI
이면putString
메서드를 사용하여Data
객체에 추가합니다. 이 메서드는 키와 값을 사용합니다.Constants
클래스의 문자열 상수KEY_IMAGE_URI
를 사용할 수 있습니다.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
메서드를 변경합니다.
- 새
OneTimeWorkRequest.Builder
를 만듭니다. setInputData
를 호출하여createInputDataForUri
의 결과를 전달합니다.OneTimeWorkRequest
를 빌드합니다.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
객체에서 가져오도록 BlurWorker
의 doWork()
메서드를 업데이트하겠습니다.
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 체인을 만들 때 유용합니다. 방법은 다음과 같습니다.
- 새
Data
를 만들고 입력의 경우와 마찬가지로outputUri
를String
으로 저장합니다. 같은 키(KEY_IMAGE_URI
)를 사용합니다. Worker
의Result.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가 필요할 수도 있습니다.
수고하셨습니다. WorkManager
를 사용하여 입력 이미지를 블러 처리했습니다.
지금은 단일 작업, 즉 이미지 블러 처리만 하고 있습니다. 훌륭한 첫 단계이지만 일부 핵심 기능이 다음과 같이 누락되었습니다.
- 임시 파일을 정리하지 않음
- 이미지를 실제로 영구 파일에 저장하지 않음
- 사진의 블러 처리 양이 항상 같음
WorkManager 작업 체인을 사용하여 위의 기능을 추가합니다.
WorkManager를 사용하면 순서대로 실행되거나 동시에 실행되는 별도의 WorkerRequest
를 만들 수 있습니다. 이 단계에서는 다음과 같은 작업 체인을 만듭니다.
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
입니다.
이 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
체인을 실행하도록 BlurViewModel
의 applyBlur
메서드를 수정해야 합니다. 현재 코드는 다음과 같습니다.
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 체인이 생성되고 실행됩니다.
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();
}
그러면 컴파일되고 실행됩니다. 블러 처리하도록 선택한 이미지가 이제 사진 폴더에 저장된 것을 확인할 수 있습니다.
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();
}
잘하셨습니다. 이제 원하는 양만큼 이미지를 블러 처리할 수 있습니다. 정말 유용하죠.
체인을 사용해 봤습니다. 이제 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
의 상태를 가져올 수 있습니다. WorkInfo
는 WorkRequest
의 현재 상태에 관한 다음과 같은 세부정보가 포함된 객체입니다.
- 작업의 상태(
BLOCKED
,CANCELLED
,ENQUEUED
,FAILED
,RUNNING
,SUCCEEDED
중에서) WorkRequest
가 완료된 경우 작업의 모든 출력 데이터
다음 표에서는 LiveData<WorkInfo>
객체나 LiveData<List<WorkInfo>>
객체를 가져오는 세 가지 방법과 각 결과를 설명합니다.
유형 | WorkManager 메서드 | 설명 |
ID를 사용하여 작업 가져오기 |
| 각 |
고유 체인 이름을 사용하여 작업 가져오기 |
| 방금 본 것처럼 |
태그를 사용하여 작업 가져오기 |
| 마지막으로, 선택적으로 WorkRequest를 String으로 태그 지정할 수 있습니다. 동일한 태그를 사용하여 여러 |
SaveImageToFileWorker
WorkRequest
를 태그 지정하면 getWorkInfosByTagLiveData
를 사용하여 가져올 수 있습니다. WorkManager ID를 사용하는 대신 태그를 사용하여 작업의 라벨을 지정하겠습니다. 왜냐하면 사용자가 여러 이미지를 블러 처리하는 경우 모든 이미지 저장 WorkRequest
의 태그가 같지만 ID는 같지 않기 때문입니다. 또한 태그를 선택할 수도 있습니다.
getWorkInfosForUniqueWorkLiveData
를 사용하지 않습니다. 모든 블러 WorkRequest
및 정리 WorkRequest
의 WorkInfo
도 반환하기 때문입니다(이렇게 반환하려면 이미지 저장 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
를 가져올 수 있습니다.
LiveData<List<WorkInfo>>
인mSavedWorkInfo
라는 새 변수를 선언합니다.BlurViewModel
생성자에서WorkManager.getWorkInfosByTagLiveData
를 사용하여WorkInfo
를 가져옵니다.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 표시
WorkInfo
의 LiveData
를 가져왔으며 이제 BlurActivity
에서 관찰할 수 있습니다. 관찰자에서 다음을 따릅니다.
WorkInfo
목록이 null이 아닌지, 그리고 목록에WorkInfo
객체가 있는지 확인합니다. 없으면 적용 버튼을 아직 클릭하지 않은 것이므로 돌아갑니다.- 목록의 첫 번째
WorkInfo
를 가져옵니다. 작업 체인을 고유하게 만들었으므로TAG_OUTPUT
으로 태그 지정된WorkInfo
는 하나만 있습니다. workInfo.getState().isFinished();
를 사용하여 작업이 완료 상태인지 확인합니다.- 완료되지 않은 경우
showWorkInProgress()
를 호출하여 적절한 뷰를 숨기고 표시합니다. - 완료된 경우
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단계 - 앱 실행
앱을 실행합니다. 앱이 컴파일되고 실행되지만 이번에는 작동할 때 진행률 표시줄이 표시되고 취소 버튼도 표시됩니다.
각 WorkInfo
에는 저장된 최종 이미지가 있는 출력 Data
객체를 가져올 수 있는 getOutputData
메서드도 있습니다. 표시할 준비가 된 블러 처리된 이미지가 있으면 항상 See File이라는 버튼을 표시해 보겠습니다.
1단계 - mOutputUri 만들기
BlurViewModel
에서 최종 URI의 변수를 만들고 이 변수의 getter와 setter를 제공합니다. String
을 Uri
로 변환하려면 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
관찰자에 적용해야 하는 몇 가지 최종 조정이 있습니다.
WorkInfo
가 완료된 경우workInfo.getOutputData().
를 사용하여 출력 데이터를 가져옵니다.- 그런 다음 출력 URI를 가져옵니다.
Constants.KEY_IMAGE_URI
키를 사용해 저장된다는 점을 기억하세요. - 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 버튼이 표시됩니다. 이 버튼을 클릭하면 출력 파일로 이동합니다.
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단계 - 작업 실행 및 취소
앱을 실행합니다. 앱이 문제없이 컴파일됩니다. 사진 블러 처리를 시작한 다음 취소 버튼을 클릭합니다. 전체 체인이 취소됩니다.
또한, WorkManager
는 Constraints
를 지원합니다. 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 창에서 충전 상태를 변경할 수 있습니다.
기기가 충전 중이 아닐 때는 기기를 연결할 때까지 계속 로드 중 상태여야 합니다.
축하합니다. 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 문서를 참고하세요.