https://developer.android.com/codelabs/kotlin-android-training-work-manager?hl=ko#0
Android Kotlin Fundamentals: WorkManager | Android Developers
In this codelab, you learn how to use WorkManager to schedule background tasks in an efficient and optimized way in your Android Kotlin app.
developer.android.com
WorkManager 개념
안드로이드 어플리케이션 아키텍쳐 구성 중 하나로써 안드로이드 JetPack의 일부분이다.
보통 *Background Task 를 효율적이고 최적화되게 처리하기 위해 WorkManager 를 사용한다.
(Background Tast = Upload file to Server, Download file from Server, Send log to Server, Sava Data in Database, etc...)
즉 WokrManage는
1.뒤로 연기할 수 있는 Background Task 들을
(즉각적으로 실행되지 않는, 그리고 foreground에서 실행되지 않는 Task)
2.설정한 환경 내에서 수행할 수 있게 만드는 방법이다.
(예를 들어, 배터리가 충전중일 때, 네트워크가 사용 가능할 때, 휴대폰을 사용하고 있지 않을 때 등, 개발자가 Work이 실행되기를 바라는 환경속에서 수행될 수 있게 만든다. 심지어 앱이 종료되거나 앱이 재시작된 상태에서도.)
2번과 같은 이유로 WorkManager는 기기의 배터리 상태와 호환성 문제를 고려해야 한다.
API 버전이 14이상인 기기에게 WorkManager는 제공되며, Work을 Scheduling할 때 API 수준에 따라서 적절한 Scheduling을 실시할 수 있다.
※주의
WorkManager는 즉각적인 앱 실행을 위해 쓰여져야 하는 Work을 위해서 사용되서는 안된다. 아키텍쳐에 따라서 반드시 Background Task를 위해서 사용되어야 하고, 앱 종료시 또는 앱 재시작 시 종료되도 상관없는 (다시 실행될 필요가 없는 Work) 을 위해서도 사용되서는 안된다.
WorkManager를 수행하는 개략적인 순서.
- Worker를 생성.
Worker는 work의 단위를 의미. 실제로 해야될 작업. - WorkRequest 생성.
work이 언제 어떻게 수행 되어야할 지 정의 - WorkManager 생성.
Worker와 WorkRequest를 가지고 Work Schedule을 관장.
Gradle Dependency For WorkManager
//work-version 2.7.1
implementation "androidx.work:work-runtime-ktx:$work-version"
Worker Class
- Worker Class : WorkManager 라이브러리 내에 클래스
background에서 실행되어야 하는 actual work이 정의되는 클래스이다. 해당 class를 상속하여 override fun doWork 메서드를 통해 실행해야 하는 업무를 구현한다.
- Worker 생성
해당 예제에서는 DevByteVideo를 Background에서 fetch하는 Worker를 구현한다. 따라서 기존에 있던 코드 (repository로 부터 web service요청을 하고, 받은 응답을 database에 저장하는 과정)를 그대로 사용한다.work package 생성
CoroutineWorker를 상속받는 class RefreshDataWorker를 정의한다. 그러면 doWork() 메소드를 override할 수 있는데, 해당 메서드는 suspend function로 정의되는데 coroutine 환경 내에서 사용할 메서드이기 때문이다.
※만약 Coroutine환경 내에서 사용되지 않는 Worker를 사용해야 한다면 , CoroutineWoker 가 아닌 Worker를 상속해야 한다.
class RefreshDataWorker(appContext: Context, param: WorkerParameters) : CoroutineWorker(appContext, param) { override suspend fun doWork() : Result { val database = getDatabase(applicationContext) val repository = VideosRepository(database) try { repository.refreshVideos() Timber.d("Work Request for Sync is run") } catch (e:HttpException) { return Result.retry() } return Result.success() } }
이후 해당 메서드 내에 수행할 업무의 코드를 구현한다.- doWork () 메서드는 *ListenableWorker Result object를 반환해야 한다.
위 코드를 보면 return 에
Android 시스템은 WorkManager에게 Work을 수행할 10분 이라는 시간을 주고, 10분이 지나면 해당 Work이 성공했는지, 실패했는지, 아니면 다시 처리해야 하는지 알아내기 위해 WorkListenableWorker Result Object를 반환하게 만든다.
WorkListenableWorker Result Object 은 다음과 같은 Static Method를 호출하여 반환한다.
- Result.success()
: 성공적으로 Work을 완료. - Result.failure()
:Work 수행에 실패 - Result.retry()
:Work 수행에 일시적인 ERROR 가 발생하여 다시 수행해야 함.
- Result.success()
WorkRequest
Worker가 일의 단위(실제 업무 Work)를 정의한다면, WorkRequest는 Work이 언제, 어떻게 실행될 지 정의합니다.
WorkRequest를 생성하는 두가지 방식.
- OneTimeWorkRequest Class
One-Off Task 용도 : 한번만 일어나고 종료되는 Work에 사용된다. - PeriodicWorkRequest Class
설정한 간격(Interval)을 두고 반복되는 Work에 사용된다. (간격 설정은 최소 15분)
WorkRequest 설정.
어떠한 Wokr을 WorkRequest를 통해 반복적으로 실행 시키고 싶다면 Application Class(및 하위(서브) 클래스) 에서 Work Manager를 통해 해당 Work을 Scheduling하는 것이 적합하다.
Application Class와 서브 클래스는 app 내 모든 activities 및 service 를 포함하는 기본 class이다. 따라서 app이 프로그램 또는 패키지를 생성하려고 할 때 Application class가 가장 먼저 객체화 된다.
따라서 Application Class를 상속하는 DevByteApplication Class 를 정의하고 해당 클래스 내에 WorkRequest를 정의한다.
class DevByteApplication : Application() {
private fun setupRecurringWork() {
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(1,TimeUnit.DAYS)
.build()
}
}
- private fun setupRecurringWork() {}
해당 메서드 내에 WorkRequest를 정의하고 추후에 WorkManager를 생성한다. - val repeatingRequest : PeriodicWorkRequest<RefreshDataWorker>
해당 변수는 PeriodiceWorkRequest 타입으로 Builder를 통해서 초기값을 생성해야한다.
Builder의 제네릭 타입을 커스텀한 Worker Class 타입을 설정해주고,
매개변수로 Interval를 설정해줘야 한다. (Interval 시간, 시간 단위)
PeriodicWorkRequestBuilder<RefreshDataWorker>(1,TimeUnit.DAYS).build()
은 하루에 한번씩 RefreshDataWoker가 실행되게끔 요청할 수 있다.
WorkManager
WorkManager는 이전까지 생성한 Worker와 WorkRequest를 가지고 work을 스케쥴링을 할 수 있다.
WorkManager 또한 Application Class에서 정의되고 실행된다.
WorkManager 생성
WorkManager는 enqueue 메서드를 통해 Work을 queue내에 (아마 queue는 Work을 run하는 cpu의 queue로 생각된다.) push할 수 있다. 또한 enqueueUniquePeriodicWork() 메서드를 이용해서 좀 더 Unique한 Work (고유한 이름이 붙여진) 을 push할 수 있다.
이는 setupRecurringWork() 메서드 내에 정의된다.
WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork(
RefreshDataWorker.WORK_NAME,
ExisitingPeriodicWorkPolicy.KEEP,
repeatingRequest
)
- WorkManager.getInstance(applicationContext)
WorkManager 는 getInstance 메서드를 통해서 싱글톤 객체를 만들 수 있다. applicationContext를 매개변수로 넘겨 싱 WorkManager의 싱글톤 객체를 만든다. - enqueueUniquePeriodicWork()
해당 메서드를 사용하기 위해서는 3가지 매개변수가 필요하다.
1) 수행해야 할 Work의 고유 이름 - RefreshDataWorker 내에 companion object로 클래스의 경로를 딴 문자열을 만든다.
//In RefreshDataWorker class companion object { const val WORK_NAME = "com.example.android.devbyteviewer.work.RefreshDataWorker" }
: RefreshDataWorker.WORK_NAME
2) ExisitingPeriodicWorkPolicy - 이미 대기중인 작업에 대하여 어떻게 처리할지 설정 :
: ExisitingPeriodicWorkPolicy.KEEP (대기중인 작업을 유지하고 새로 넣을 작업은 폐기 시키는 전략.)
3) WorkRequest - 이전에 정의한 WorkRequest를 넘겨준다. (당연히 1번 매개변수의 Worker와 WorkeRequest의 제네릭 타입이 같아야 한다.)
: repeatingRequest
WorkManager 실행
DevByteApplication class내에 WorkManager와 더불어 background 에서 실행되어야 할 코드를 구현한다.
private val applicationScope = CoroutineScope(Dispatchers.Default)
private fun delayedInit() {
applicationScope.launch{
setupRecurringWork()
}
}
...
override fun onCreate() {
super.onCreate()
delayedInit()
}
- val applicatonScope : CoroutineScope
Dispatchers.Default 를 매개변수로 넘겨 생성된 코루틴 스코프를 사용한다. Default 디스패쳐와 IO 디스패쳐와는 또 차이점이 있다. (참고 블로그 https://sandn.tistory.com/110) - private fun delayedInit() {}
해당 함수를 통해 setupRecurringWork을 실행한다. - override fun onCreate() {}
해당 함수를 통해 delayedInit() 을 실행한다. delayedInit() 내에 코드들은 Default Dispatcher에서 실행되기 때문에 foreground를 방해하지 않는다.
Constraints
WorkRequest는 언제 어떻게 Work이 실행되는지 정의한다. 위에서는 "언제" Work이 실행되는 지에 초첨을 맞췄다면 이번엔 Constraints를 통해 Work이 어떻게 어떤 환경에서 실행될 지 정의하는 과정을 살펴본다.
WorkRequest에 Constaints 추가
WorkRequest에 Constraint를 추가하기 위해선 역시 WorkRequest가 정의된 setupRecurringWork() 메서드 내에 정의된다. Constraints.Builder를 사용하여 Constraints를 정의한다.
//In DevByteApplication class, at setupRecurringWork() Method
val constraints = ConstraintsBuilder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
.setRequiresCharging(true)
.apply{
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setRequiresDeviceIdle(true)
}
}
.build()
- setRequiredNetworkType()
해당 메서드는 네트워크의 상태에 따른 Work 실행환경을 설정한다. 만약 NetworkType.UNMETERED 로 설정이 됐다면 해당 네트워크가 (켜진상태 + 광역제한이 없는 상태) 이어야 Work을 실행할 수 있다는 뜻이다. - setRequiresBatteryNotLow()
해당 메서드는 기기의 배터리 상태가 낮지 않은 (30%? 50%?) 상태에서 Work을 실행할 지 결정한다. true면은 설정된 배터리 수치 이하로 떨어지면 Work은 실행될 수 없다. - setRequiresCharging()
해당 메서드는 기기가 충전중일 때 Work을 실행할지 결정한다. true 일 경우 충전중일 때 Work을 실행한다. - setRequiresDeviceIdle()
device가 사용중이지 않을 때 (non action) Work을 실행할지 결정한다. 해당 메서드는 API버전이 m(마쉬멜로우) 이상일 경우에만 사용이 가능하다. 따라서 apply{} 를 통해 Build VERSION을 확인 후, 메서드를 사용하도록 한다. true일 경우 기기를 사용 중이지 않을 때 Work을 실행한다.