[공부하게된 계기]
안드로이드 공부를 시작하고 스레드를 이용한 적은 있지만 앱 프로세스가 죽어서도 백그라운드에서 작업하게 할 일이 없었다.
하지만 최근 타이머 관련 오픈소스 작업을 시도하며, 그리고 과제전형 문제를 풀면서도 백그라운드 작업을 공부하게 되었는데 백그라운드 관련 라이브러리 중에서 최근 가장 핫한 WorkManager 를 공부해야겠다고 생각했다.
[그래서 WorkManager 뭘 하는 건가?]
백그라운드 세 가지 작업종류의 persistent work 를 다루는데 추천되는 라이브러리이다.
백그라운드 작업의 종류에 관해서는 내가 전에 써놓은 아래링크를 참고하면 된다.
persistent 하다는 말은 work 가 스케줄되면 앱이 재시작하거나 시스템이 재부팅해도 지속된다는 것이다.
즉 앱이 종료되거나 기기가 다시 시작되어도 실행예정이고, 지연 가능한 비동기 작업을 쉽게 예약해주는 것이 WorkManager 이다.
Android 에서 권장되는 작업 스케줄러로, 지연 간으한 작업을 실행하도록 보장해준다.
[장점]
예전에는 위 기능을 개발자가 API 레벨에 따라 다르게 구현해주어야해서 성가셨는데 (API 23 이상은 JobScheduler, 미만은 BroadcastReceiver + AlarmManager)
WorkManager 가 이를 내부적으로 처리해준다는 장점이 있다.
[언제 쓸까?]
[WorkManager 의 특징]
- Work constraints 로 work 가 기기 상태에 따라 최적의 조건에서 실행되도록 정의할 수 있다.
- work 가 한 번만 실행되게 할 수도 있고, 반복적으로 실행되게 할 수도 있다.
- work 는 태그될 수 있어서, work 를 특정짓고 대체가능하게 스케줄 할 수 있고, 취소할 수있다.
- 스케줄된 work 는 내부적으로 SQLite database 와 WorkManager 에서 관리된다.
- 동시 작업 실해을 포함한 복잡한 작업 요청 체이닝
- 한 작업 요청의 출력이 다음 작업 요청의 입력으로 사용할 수 있음
- Google Play 서비스 사용 여부와 관계없이 작동
- LiveData 지원
- JobScheduler 및 AlarmManager 등 몇 가지 API 위에 있다.
- exponentail backoff policy 를 포함한, 유연한 재시도 정책을 제공하고
[지속적인 작업 유형 세 가지]
- Immediate: 즉시 시작하고, 한 번만, 신속하게 처리될 수 있는 일
OneTimeWorkRequest 와 Worker 로 접근할 수 있고
신속 작업 처리의 경우 setExpedited 를 호출한다. - Long Running : 10분 이상 더 오래 실행될 수 있는 작업
모든 WorkRequst 또는 Worker 로 접근할 수 있다.
알림을 처리하고 싶다면 setForeground() 를 호출한다. - Defferable : 나중에 시작하여 주기적으로 실행될 수 있는 예약된 작업
PeriodicWorkRequest 및 Worker 로 접근할 수 있다.
[기본 사용법]
- Worker 클래스 를 상속한 클래스를 만든다.
- 해야할 일을 doWork 를 오버라이딩 하여 작성한다.
- WorkManager.getInstance(context) 로 싱글톤 인스턴스를 찾는다.
- .enqueue 를 호출한다. .enqueue(~Request.from(WorkerName::class.java) 와 같이 리퀘스트 인자를 넣어준다.
- 다른 설정을 하고 싶다면 ~RequestBuilder<> 의 지네릭스 타입에 WorkerName 을 넣고, Builder 패턴으로 ~RequestBuilder<> 의 메소드를 연달아 호출한 후, 마지막에 .build 로 객체를 생성해준 다음 WorkManager 객체의 인스턴스의 enqueue 인자로 전달하면 된다.
- 예를들어 WorkRequest 에 입력을 추가할 수 있는데 Data 를 ~RequestBuilder 에 넣어주는 메소드를 호출해서 넣어주는 방법이 있다. Data 도 마찬가지로 builder 패턴으로 만들면된다.
// 위 내용에 대한 예시코드
// ViewModel 클래스
...
private fun timerWorkerTest() {
workManager.enqueue(setDataRequestTest())
}
private fun setDataRequestTest() =
OneTimeWorkRequestBuilder<TestWorker>()
.setInputData(createDataTest())
.build()
private fun createDataTest() =
Data
.Builder()
.putString(KEY_TEST_WORKER_DATA, "시간이 다 되었어요.")
.build()
...
// Worker 클래스
class TestWorker(context: Context, params: WorkerParameters): Worker(context, params) {
override fun doWork(): Result {
val text = inputData.getString(KEY_TEST_WORKER_DATA)
Log.d("TimerWorker", "TimerWorker 호출됨 : $text") // 로그가 잘 찍힘을 확인했다.
return Result.success()
}
}
- Result.success 메소드의 인자로 Data 객체를 넣어줌으로서 출력데이터를 추가할 수 있다.
- 또한 WorkRequest 를 체인으로 실행하도록 하고 싶으면 WorkManager.enqueue 메소드가 아니라 WorkManager.beginWith 와 then 메소드를 활용해줘야한다. 이가 가능한 이유는 beginWith 와 then 모두 WorkContinuation 인스턴스를 반환하기 때문이다.
// ViewModel
...
private fun timerWorkerChainTest() {
workManager
.beginWith(
OneTimeWorkRequestBuilder<TestWorker>()
.setInputData(createDataByStringTest("1"))
.build()
)
.then(OneTimeWorkRequest.from(TestWorker::class.java)) // Data 전달 없음 주의
.then(OneTimeWorkRequest.from(TestWorker::class.java)) // 이전 Worker 의 output data 를 사용한다.
.enqueue() // 실질적인 work 의 시작은 enqueue
}
private fun createDataByStringTest(input: String) =
Data
.Builder()
.putString(KEY_TEST_WORKER_DATA, input)
.build()
...
// Worker
class TestWorker(context: Context, params: WorkerParameters): Worker(context, params) {
override fun doWork(): Result {
// Worker 에 집중하기 위해 try catch 문 생략
val number = inputData.getString(KEY_TEST_WORKER_DATA)?.toInt()
Log.d("TestWorker", "TestWorker 호출됨 : $number")
val outputData = workDataOf(KEY_TEST_WORKER_DATA to "${number!! + 1}")
return Result.success(outputData)
}
}
그럼 아래와 같이 로그가 찍힌다.
TestWorker 호출됨 : 1
TestWorker 호출됨 : 2
TestWorker 호출됨 : 3
- 같은 체인을 여러번 사용할 때 한 번에 하나의 체인만 실행시키고 싶다면 beginUniqueWork 메소드를 사용한다. d인자는 체인 이름, ExistingWorkPolicy enum 값 그리고 beginWith 에 넣어주었던 Request 이다.
- ~RequestBuilder<>() 의 build() 전에 addTag(string) 을 넣어줌으로서 태그를 달 수 있다. 그리고 그 chain 을 구분할 수 있다.
- WorkManager 객체의 getWorkInfoByIdLiveData, getWorkInfosForUniqueWorkLiveData, getWorkInfosByTagLiveData 메소드를 통해 LiveData<WorkInfo> or LiveData<List<WorkInfo>> 객체를 가져올 수 있고 observe 할 수 있다. (WorkInfo 는 WorkRequest 의 현재 상태에 대한 자세한 정보를 담고 있는 객체이다. work 의 현재상태를 알 수 있고, WorkRequest 가 끝났을 때 그 Work 로부터의 output data 를 얻을 수 있다.
workInfo.state.isFinished 로 끝난지 알 수 있다. 그리고 workInfo.outputData.getString 으로 데이터를 가져오는 방법이 있다. - 아래는 재미로 찍어본 로그 코드와 그 결과이다.
// ViewModel
...
internal val workInfosByTag: LiveData<List<WorkInfo>>
internal val workInfosByChainName: LiveData<List<WorkInfo>>
companion object {
const val WORK_CHAIN_NAME = "work_chain_name"
const val WORK_INFO_BY_TAG = "workInfo by tag"
}
init {
getCoins()
//timerWorkerChainTest()
workInfosByChainName = workManager.getWorkInfosForUniqueWorkLiveData(WORK_CHAIN_NAME)
workInfosByTag = workManager.getWorkInfosByTagLiveData(WORK_INFO_BY_TAG)
}
fun timerWorkerChainTest() {
workManager
.beginUniqueWork(
WORK_CHAIN_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequestBuilder<TestWorker>()
.addTag(WORK_INFO_BY_TAG)
.setInputData(createDataByStringTest("1"))
.build()
)
.then(
OneTimeWorkRequestBuilder<TestWorker>()
.addTag(WORK_INFO_BY_TAG)
.build()
)
.then(
OneTimeWorkRequestBuilder<TestWorker>()
.addTag(WORK_INFO_BY_TAG)
.build()
) // 반복문으로 쓸 수도 있음! 보여주기식이라 비효율적이게 보일 수 있다.
.enqueue() // 실질적인 work 의 시작은 enqueue
}
private fun createDataByStringTest(input: String) =
Data
.Builder()
.putString(KEY_TEST_WORKER_DATA, input)
.build()
...
// Activity onCreate
...
viewModel.workInfosByTag.observe(this) { listOfWorkInfo ->
Log.e(LOG_TAG_TEST_WORKER, "workInfosByTag :\n ${listOfWorkInfo.joinToString("\n") { "$it" }}")
}
viewModel.workInfosByChainName.observe(this) { listOfWorkInfo ->
Log.e(LOG_TAG_TEST_WORKER, "workInfosByChainName :\n ${listOfWorkInfo.joinToString("\n") { "$it" }}")
}
binding.btEnqueueWork.setOnClickListener {
viewModel.timerWorkerChainTest()
}
...
//Worker
class TestWorker(context: Context, params: WorkerParameters): Worker(context, params) {
override fun doWork(): Result {
sleep(5000L) // 로그를 천천히 보려고 넣었다.
val number = inputData.getString(KEY_TEST_WORKER_DATA)?.toInt()
Log.e(LOG_TAG_TEST_WORKER, "TestWorker 호출됨 : $number")
val outputData = workDataOf(KEY_TEST_WORKER_DATA to "${number!! + 1}")
return Result.success(outputData)
}
}
E/log tag test worker: workInfosByTag :
WorkInfo{mId='2d776695-af9e-4d55-9608-dc20c9a43478', mState=RUNNING, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='8ce586c3-c04a-4156-af9b-e0ac6e976ea4', mState=BLOCKED, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='c02b2a21-4df6-49a0-bcc7-e6e2bd365605', mState=BLOCKED, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
E/log tag test worker: workInfosByChainName :
WorkInfo{mId='2d776695-af9e-4d55-9608-dc20c9a43478', mState=RUNNING, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='8ce586c3-c04a-4156-af9b-e0ac6e976ea4', mState=BLOCKED, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='c02b2a21-4df6-49a0-bcc7-e6e2bd365605', mState=BLOCKED, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
E/log tag test worker: TestWorker 호출됨 : 1
I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=2d776695-af9e-4d55-9608-dc20c9a43478, tags={ workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker } ]
I/WM-WorkerWrapper: Setting status to enqueued for c02b2a21-4df6-49a0-bcc7-e6e2bd365605
E/log tag test worker: workInfosByTag :
WorkInfo{mId='2d776695-af9e-4d55-9608-dc20c9a43478', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 2, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='8ce586c3-c04a-4156-af9b-e0ac6e976ea4', mState=BLOCKED, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='c02b2a21-4df6-49a0-bcc7-e6e2bd365605', mState=ENQUEUED, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
E/log tag test worker: workInfosByChainName :
WorkInfo{mId='2d776695-af9e-4d55-9608-dc20c9a43478', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 2, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='8ce586c3-c04a-4156-af9b-e0ac6e976ea4', mState=BLOCKED, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='c02b2a21-4df6-49a0-bcc7-e6e2bd365605', mState=RUNNING, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
E/log tag test worker: workInfosByTag :
WorkInfo{mId='2d776695-af9e-4d55-9608-dc20c9a43478', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 2, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='8ce586c3-c04a-4156-af9b-e0ac6e976ea4', mState=BLOCKED, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='c02b2a21-4df6-49a0-bcc7-e6e2bd365605', mState=RUNNING, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
E/log tag test worker: TestWorker 호출됨 : 2
I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=c02b2a21-4df6-49a0-bcc7-e6e2bd365605, tags={ workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker } ]
I/WM-WorkerWrapper: Setting status to enqueued for 8ce586c3-c04a-4156-af9b-e0ac6e976ea4
E/log tag test worker: workInfosByTag :
WorkInfo{mId='2d776695-af9e-4d55-9608-dc20c9a43478', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 2, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='8ce586c3-c04a-4156-af9b-e0ac6e976ea4', mState=ENQUEUED, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='c02b2a21-4df6-49a0-bcc7-e6e2bd365605', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 3, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
E/log tag test worker: workInfosByChainName :
WorkInfo{mId='2d776695-af9e-4d55-9608-dc20c9a43478', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 2, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='8ce586c3-c04a-4156-af9b-e0ac6e976ea4', mState=RUNNING, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='c02b2a21-4df6-49a0-bcc7-e6e2bd365605', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 3, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
E/log tag test worker: workInfosByTag :
WorkInfo{mId='2d776695-af9e-4d55-9608-dc20c9a43478', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 2, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='8ce586c3-c04a-4156-af9b-e0ac6e976ea4', mState=RUNNING, mOutputData=Data {}, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='c02b2a21-4df6-49a0-bcc7-e6e2bd365605', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 3, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
E/log tag test worker: TestWorker 호출됨 : 3
I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=8ce586c3-c04a-4156-af9b-e0ac6e976ea4, tags={ workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker } ]
E/log tag test worker: workInfosByTag :
WorkInfo{mId='2d776695-af9e-4d55-9608-dc20c9a43478', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 2, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='8ce586c3-c04a-4156-af9b-e0ac6e976ea4', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 4, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='c02b2a21-4df6-49a0-bcc7-e6e2bd365605', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 3, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
E/log tag test worker: workInfosByChainName :
WorkInfo{mId='2d776695-af9e-4d55-9608-dc20c9a43478', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 2, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='8ce586c3-c04a-4156-af9b-e0ac6e976ea4', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 4, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
WorkInfo{mId='c02b2a21-4df6-49a0-bcc7-e6e2bd365605', mState=SUCCEEDED, mOutputData=Data {key_timer_worker_data : 3, }, mTags=[workInfo by tag, com.nosorae.labs.clean_architecture.hilt.background.work_manager.work.TestWorker], mProgress=Data {}}
- Work 를 취소하는 것은 WorkInfo 를 담은 LiveData 를 가져오는 것과 마찬가지로 id or tag or unique chain name 으로 할 수 있다. WorkManager.cancel~
- Constraints 를 만들고 적용하는 법
Constraints.Builder() 의 set~ 메소드를 호출하고 build() 해준다.
그 후 ~RequestBuilder setConstraint 에 앞서 만든 Constraints 를 넣어주면 된다.
[Reference]
CodeLabs
'Android > 이론 학습' 카테고리의 다른 글
[Android/Firebase] 기존 프로젝트에 Crashlystics 적용하기 (0) | 2022.03.06 |
---|---|
[Kotlin/백준] 5430, AC (시간초과 해결) (0) | 2022.02.27 |
[Android] Service 의 타입과 사용법 그리고 권장사항 (0) | 2022.02.23 |
[Android/Kotlin] Intent 에 커스텀 객체 전달하기? 직렬화, 역직렬화 Serializable 과 Parcelable, @Parcelize (0) | 2022.02.20 |
[Android] Hilt 의 다양한 어노테이션을 알아보자 (0) | 2022.02.19 |