엄코딩의 개발 일지

Migrating from Firebase JobDispatcher to WorkManager

WorkManager 동작 원리

Note: If your app targets Android 10 (API level 29) or above, your FirebaseJobDispatcher and GcmNetworkManager API calls will no longer work on devices running Android Marshmallow (6.0) and above. Follow the migration guides for FirebaseJobDispatcher and GcmNetworkManager for guidance on migrating. Also, see the Unifying Background Task Scheduling on Android announcement for more information regarding their deprecation.

 

developer.android.com/topic/libraries/architecture/workmanager

 

WorkManager로 작업 예약  |  Android 개발자  |  Android Developers

WorkManager로 작업 예약  Android Jetpack의 일부 WorkManager는 지연 가능한 비동기 작업을 쉽게 예약할 수 있는 API로, 지연 가능한 비동기 작업은 앱이 종료되거나 기기가 다시 시작되더라도 실행될 것

developer.android.com

 

결론은 targetSDK 30일 경우에 FirebaseJobDispatcher, GcmNetworkManager을 사용중이라면 WorkManager로 마이그레이션해야한다.

 

마이그레이션 방법은 간단하다. 기존에 JobService를 사용하였다면 ListenableWorker로 변경해주면된다. 

코드 변경은 google docs를 보고 따라하면 충분하지만, 내부 동작이나 클래스들을 조금 더 살펴볼 필요가 있다.

 

WorkManager 특징

1. Job을 스케줄링 할 수 있다.

2. Job의 상태를 관찰 할 수 있다.

3. 제약조건을 두어, 해당 제약조건을 만족해야 Job이 동작할 수 있도록 설계할 수 있다.

4. Job들을 원하는 순서대로 실행되도록 chain할 수 있다.

 

Robust Scheduling

  • 작업은 내부적으로 관리되는 SQLite 데이터베이스에 저장, WorkManager는 이 작업이 지속되고 기기 재부팅시 일정이 재조정되도록 한다.
  • 절전 기능, Doze 모드 정책을 따라서 시행된다. ( 이전 도즈모드일 경우 FCM, AlarmManager 사용 )

 

주요 Class

WorkManager : 실행할 Job들을 enqueue하는 역할과 작업을 chain하는 역할을 담당한다.

Worker : 백그라운드에서 동작하고, Result를 반환하는 doWork 메소드를 가진다.

ListenableWorker : 메인스레드에서 동작하고, ListenableFuture<Result>를 반환하는 startWork 메소드를 가진다.

WorkRequest : Worker에 태그를 추가하거나, 특정 제약조건을 설정할지 명시한다.

Data : Worker들은 Data를 통해 데이터를 주고 받을 수 있다.

 

Result

  • Result.success() : 작업이 성공적으로 완료되었음을 의미한다.
  • Result.failure() : 작업이 실패로 끝났고, 다시 시작하지 않아도 됨을 의미한다.
  • Result.retry() : 작업이 실패로 끝났고, 다시 시작해야 함을 의미한다.

Cancel

WorkManager 에서 취소 하고자 하는 작업이 이미 완료 된 작업이라면 취소 메서드는 아무 기능도 하지 않는다. 아직 실행 전 큐에 담긴 상태라면 실행하지 않고 취소 된다.

 

예제 코드

WorkManager.getInstance(...)
    .beginWith(listOf(workA,workB))
    .then(workC)
    .enqueue()

위와 같은 코드를 작성한 경우 workA와 workB는 동시에 실행된다. (순차적이지 않다.) 이후 workC가 실행된다.

(workA와 workB가 성공적으로 완료되어야만 workC가 동작한다. )

 

우선 beginWith에 존재하는 작업들이 완료가 되면 세팅되어있던 InputMerger를 통해 데이터가 생성된다.

InputMerger는 추상화 클래스이고 이를 구현한 OverwritingInputMerger, ArrayCreatingInputMerger가 존재한다.

 

이름에서 느껴지듯이 전자는 결과를 overwrite하고, 후자는 배열로 합쳐준다.

 

OverwritingInputMerger

 

ArrayCreatingInputMerger

var firstReq: OneTimeWorkRequest =OneTimeWorkRequestBuilder<FirstWorker>()
        .addTag(TAG)
        .setInputMerger(ArrayCreatingInputMerger::class.java)
        .build()

InputMerger를 세팅하는 방법은 WorkerRequest를 생성할 때 위와 같은 코드를 작성하면 된다.

 

WorkQuery

val workQuery = WorkQuery.Builder
//                        .fromUniqueWorkNames(listOf(TAG))
                        .fromIds(listOf(firstReq.id, secondReq.id, thirdReq.id, fourthReq.id))
                        .addStates(listOf(WorkInfo.State.SUCCEEDED, WorkInfo.State.CANCELLED))
                        .build()

                val workInfos: ListenableFuture<List<WorkInfo>> = workManager.getWorkInfos(workQuery)


                for (currentWorker in workInfos.get()) {
                    Log.e("Seonoh", "Worker : " + currentWorker.tags)
                    Log.e("Seonoh", "State : " + currentWorker.state.name)
                }

WorkQuery는 일종의 query를 통해서 작업 상태를 관찰할 수 있다.

처음에는 주석으로 처리된 uniqueWorkName값을 사용해서 쿼리를 진행했는데, WorkManager가 내부적으로는 sqlite를 사용하여 내부에 저장되어서 이전 작업 목록들도 나오게 되어서 id 값을 통해 진행하게되었다. 

 

getLiveData

workManager.getWorkInfoByIdLiveData(fourthReq.id)
            .observe(this, androidx.lifecycle.Observer{
binding.run{
stateTv.text=it.state.name
                    val progressValue =it.outputData.getInt("Progress", 0);
                    if (progressPercent < progressValue)
                        progressTv.text= progressValue.toString()

                    workerTitleTv.text=it.tags.toString()

                    if (it.outputData.getStringArray("finishedWorker") != null) {
                        for (result init.outputData.getStringArray("finishedWorker")!!) {
                            resultText += result

                        }
                    }
                    resultTv.text= resultText
}
            })

}

getWorkInfoByIdLiveData함수등을 사용하여 작업을 observe하여 변화값에 대하여 ui처리가 가능하다.

 

ListenableWorker

마지막으로 살펴볼 부분은 ListenableWorker이다. 구글에서는 왠만하면 Worker를 이용하기를 권장하고 있다. 하지만 콜백기반의 비동기 API를 사용하는 경우 ListenableWorker를 사용해야한다.

developer.android.com/topic/libraries/architecture/workmanager/advanced/listenableworker?hl=ko#kotlin

 

ListenableWorker의 스레딩  |  Android 개발자  |  Android Developers

특정 상황에서는 맞춤설정 스레딩 전략을 제공해야 합니다. 예를 들어 콜백 기반의 비동기 작업을 처리해야 하는 경우입니다. 이때는 차단 방식으로 작업을 할 수 없기 때문에 Worker를 사용할 수

developer.android.com

우선 지금까지 학습 내용을 살펴보자.

class CallbackWorker(
        context: Context,
        params: WorkerParameters
) : ListenableWorker(context, params) {
    override fun startWork(): ListenableFuture<Result> {
        return CallbackToFutureAdapter.getFuture { completer ->
            val callback = object : Callback {
                var successes = 0

                override fun onFailure(call: Call, e: IOException) {
                    completer.setException(e)
                }

                override fun onResponse(call: Call, response: Response) {
                    successes++
                    if (successes == 100) {
                        completer.set(Result.success())
                    }
                }
            }

            repeat(100) {
                downloadAsynchronously("https://example.com", callback)
            }

            callback
        }
    }
}

getFuture() 함수에서 내부적으로 동작하는 기능이 많다.

public static <T> ListenableFuture<T> getFuture(@NonNull Resolver<T> callback) {
    Completer<T> completer = new Completer<>();
    SafeFuture<T> safeFuture = new SafeFuture<>(completer);
    completer.future = safeFuture;
    // Set something as the tag, so that we can hopefully identify the call site from the
    // toString()
    // of the future. Retaining the instance could potentially cause a leak (if it's an inner
    // class)
    // and it's probably a lambda anyway so retaining the class provides just as much
    // information.
    completer.tag = callback.getClass();
    // Start timeout before invoking the callback
    final Object tag;
    try {
        tag = callback.attachCompleter(completer);
        if (tag != null) {
            completer.tag = tag;
        }
    } catch (Exception e) {
        safeFuture.setException(e);
    }
    return safeFuture;
}

디버그를 통해 내부를 조금만 더 살펴보자.

 

Resolver

  • Resolver은 callback object를 만들고, 트리거하는데 요구되는 작업을 시작한다.

Object attachCompleter(@NonNull Completer<T> completer) throws Exception

  • 콜백 객체를 생성하고, 이를 트리거 하는 작업을 설정한다.

결국 콜백오는 시점에 completer를 통해 set(RESULT)를 하는것이다.