๐ง WorkManager?
์๋๋ก์ด๋๊ฐ ๋ฒ์ ์ ์ ๊ฑฐ๋ญํ๋ฉฐ ์ด๋ ์๊ฐ๋ถํฐ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ์ด ์์ํ์ง ์๊ฒ๋์๋ค.
์ด๊ธฐ์๋ Service๋ฅผ ์ด์ฉํด์ ์์ ํ๊ฑฐ๋ Broadcast Receiver๋ฅผ ํตํ์ฌ ํ๋ก์ธ์ค๋ฅผ ๊นจ์ฐ๋ ๊ฒ ๋ํ ์ฝ๊ฒ ๊ฐ๋ฅํ๋ค.
ํ์ง๋ง ์๋๋ก์ด๋ M(API 23)๋ถํฐ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ์ ์ํ ์ ์ฝ ์ฌํญ๋ค์ด ์ถ๊ฐ๋๋ฉฐ, ๊ฐ๋ฐ์๋ค์ ์ฌ๋ฌ API๋ฅผ ํตํ์ฌ ์ถ๊ฐ ์ฝ๋ ์์ฑ์ด ํ์ํด์ก๋ค. ์ฆ, ๋ถ๊ฐ๋ฅํ ๊ฒ์ ์๋์ง๋ง ๊น๋ค๋ก์์ก๋ค๋ ๊ฒ์ด๋ค.
๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ์ ์ฌ๋ฌ๊ฐ์ง ๋ฐฉ๋ฒ๋ค๋ก ๊ฐ๋ฅํ๋ค. AlarmManager, JobScheduler, JobDispatcher(Firebase), ๊ทธ๋ฆฌ๊ณ ์ง๊ธ ์์๋ณผ WorkManager.
์ฌ๊ธฐ์ ์ฌ๋ฌ๊ฐ์ง ๋ฐฉ๋ฒ๋ค์ด ์กด์ฌํ๋ ์ด์ ๋ ์ฉ๋(๋ชฉ์ )์ ๋ฐ๋ผ์ ๊ฐ๋ฐ์๋ค์ด ์ ์ ํ API๋ฅผ ์ ํํด ์ฌ์ฉํด์ผ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
๊ตฌ๊ธ์์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฒ๋ฆฌ์ ๋ํ ๊ฐ์ด๋๋ฅผ ์ ๊ณตํ๋ค. ํด๋น ๋ฌธ์๋ฅผ ํ์ธํ์ฌ ์๋ง์ ๋ฐฉ๋ฒ์ ์ ํํ์. ๊ณต์๋ฌธ์ - ๋ฐฑ๊ทธ๋ผ์ด๋ ๊ฐ์ด๋
WorkManager๋ ํ๋ก์ธ์ค ์ข ๋ฃ ์ฌ๋ถ์ ๊ด๊ณ์์ด ๋ฐ๋์ ์์ ์ ์คํํ๋ค. ํ์ง๋ง ์์ ์ด ์ฆ์ ์คํ๋๋ ๊ฒ์ ๋ณด์ฅํ์ง ์์์ผ๋ก ์์์ ์ธ๊ธํ ๊ฒ๊ณผ ๊ฐ์ด ์ ํฉํ ์๋ฃจ์ ์ ๊ณ ๋ฏผํด์ผํ๋ค.
์๋ฒ์ ๋ก๊ทธ ๋๋ ๋ถ์ ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๊ฑฐ๋ ์ฃผ๊ธฐ์ ์ผ๋ก ๋ก์ปฌ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ์ ๋๊ธฐํํ๋ ์์ , ์ด๋ฏธ์ง ์ ์ฅ ๋ฐ ์ ๋ก๋ ๋ฑ์ ์์ ์ด๋ผ๋ฉด WorkManager ์ฌ์ฉ์ ์ ํํด๋ ์ข๋ค.
๐ WorkManager ๊ตฌ์ฑ
- Worker
- Abstract class
- ํด๋์ค๋ฅผ ์์๋ฐ๊ณ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์คํํ๊ณ ์ ํ๋ ์ฝ๋๋ฅผ doWork() ๋ฉ์๋์ ์ ์ํ๋ค.
- ์์
์ํ๋ฅผ ๋ํ๋ด๋ Result์ ์ ์๋ success(), failure(), retry() ๋ฑ์ ๋ฉ์๋๋ฅผ ํตํด ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋ค.
๋ฐํ๋ ๊ฐ์ ๋ฐ๋ผ์ ์ดํ ๋์์ ๊ฒฐ์ ํ ์ ์๋ค. (์ฌ์๋, ์ค๋จ ๋ฑ)
- Result.Sucesss / Result.Failure / Result.Retry
- WorkRequest
- Worker์ ์ ์๋ task๋ฅผ ์๋์ํค๊ธฐ ์ํ request๋ฅผ ๋ํ๋ธ๋ค.
- WorkRequest๋ฅผ ์์ฑํ ๋ ๋ฐ๋ณต ์ฌ๋ถ, ์ ์ฝ ์ฌํญ ๋ฑ์ ์ ๋ณด๋ฅผ ๋ด๋๋ค.
- OneTimeWorkRequest
- ํ๋ฒ๋ง ์คํํ ์์ ์์ฒญ์ ๋ํ๋ด๋ WorkRequest
- PeriodicWorkRequest
- ์ผ์ ์ฃผ๊ธฐ๋ก, ์ฌ๋ฌ๋ฒ ์คํํ ์์ ์์ฒญ์ ๋ํ๋ด๋ WorkRequest
- WorkManager
- ์ค์ ๋ก WorkRequest๋ฅผ ์ค์ผ์ค๋งํ๊ณ ์คํํ๋ฉฐ ๊ด๋ฆฌํ๋ ํด๋์ค
- ์ธ์คํด์ค๋ฅผ ๋ฐ์์ WorkRequest๋ฅผ ํ์ ์ถ๊ฐํ์ฌ ์คํํ๋๋ก ํ๋ค.
๐จ ๊ตฌํํ๊ธฐ
๐ Dependency ์ถ๊ฐ
dependencies {
implementation "androidx.work:work-runtime-ktx:2.5.0" // Kotlin + Coroutines
}
Java Only ๋ฒ์ ๋๋ ์ต์ ๋ฒ์ ์ ๊ณต์ ๋ฌธ์์์ ํ์ธํ ์ ์๋ค. ๊ณต์ ๋ฌธ์ ๋ฐ๋ก๊ฐ๊ธฐ
๐ Worker ์์ฑํ๊ธฐ
Worker๋ฅผ ์์๋ฐ์ ํด๋์ค๋ฅผ ์์ฑํ๊ณ Context์ WorkerParameters๋ฅผ ์์ฑ์์ ๋๊ธด๋ค.
doWork() ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ํ๊ณ , ํ๊ณ ์ํ๋ ์์ ์ฝ๋๋ฅผ ์์ฑํ๋ค.
class SomeWorker(ctx: Context, params: WorkerParameters) :
Worker(ctx, params) { // If you want coroutines, CoroutineWorker()
override fun doWork(): Result {
return try {
Timber.d("I'm hard worker, but i will sleep") // Timber is library for logging
Thread.sleep(3_000)
Timber.d("Who woke up. I'm tired.")
Result.success() // return statement
} catch (e: Exception) {
Timber.e("Worker Exception $e")
Result.failure() // return statement
}
}
}
๐ WorkerRequest ์์ฑํ๊ธฐ
ํ๋ฒ๋ง ์คํ๋ ์์ ์ OneTimeWorkRequest, ๋ฐ๋ณต์ ์ผ๋ก ์คํ๋ ์์ ์ PeriodicWorkRequest๋ฅผ ์ฌ์ฉํ์ฌ ๋ง๋ ๋ค.
PeriodicWorkRequest์ ๊ฒฝ์ฐ ๋ฐ๋ณต ์ฃผ๊ธฐ ๋ฐ ์๊ฐ ๋จ์๋ฅผ ์ธ์๋ก ๋๊ฒจ์ผ ํ๋๋ฐ ์ต์๊ฐ์ 15๋ถ์ผ๋ก ์ ์๋์ด ์๋ค.
// in view(activity, fragment) or viewmodel
// OneTimeWorkRequest
val workRequest = OneTimeWorkRequestBuilder<SomeWorker>().build()
// PeriodicWorkRequest
val workRequest = PeriodicWorkRequestBuilder<SomeWorker>(15, TimeUnit.MINUTES).build()
๐ WorkerManager ์์ฑ ๋ฐ ํ์ ์์ ์ถ๊ฐํ๊ธฐ
WorkerManager์ ์ธ์คํด์ค๋ getInstance() ๋ฉ์๋๋ฅผ ํตํด์ ๊ฐ์ ธ์ฌ ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ enqueue() ๋ฉ์๋๋ฅผ ํตํด WorkRequest๋ฅผ ์ถ๊ฐํ ์ ์๋ค.
// in view(activity, fragment) or viewmodel
private val workManager = WorkManager.getInstance(application)
workManager.enqueue(workRequest) // ์์์ ์์ฑํ workRequest๋ฅผ ๋ฃ์ด์ค๋ค.
์์ ๊ฐ์ด ๊ฐ๋จํ๊ฒ WorkManager์ ์์ ๋ค์ ์ถ๊ฐํ์ฌ ์คํ์ํฌ ์ ์๋ค.
๐จ ๋ ์์๋ณด๊ธฐ
๐ Constraint(์ ์ฝ ์ฌํญ) ์ถ๊ฐํ๊ธฐ
WorkRequestBuilder์ ์ ์ฝ ์ฌํญ์ ์ถ๊ฐํ ์ ์๋ค. ์ ์ฝ ์ฌํญ์ Constraints.Builder()๋ฅผ ํตํด ์์ฑํ ์ ์๋ค.
๋คํธ์ํฌ ์ ํ, ์ถฉ์ ์ํ ๋ฑ์ ๋ฐ๋ผ์ WorkManager๋ฅผ ์๋์ํฌ ์ ์์ผ๋ฉฐ ๋ ์์ธํ ์ ์ฝ ์กฐ๊ฑด ์ข
๋ฅ๋ ๊ณต์ ๋ฌธ์๋ฅผ ํ์ธํ์.
๊ณต์ ๋ฌธ์ ๋ฐ๋ก๊ฐ๊ธฐ
// in view(activity, fragment) or viewmodel
val constraints = Constraints.Builder()
.setRequiresCharging(true) // ์ถฉ์ ์ํ์ผ ๋๋ง ์๋ํ๋ค.
.setRequiresBatteryNotLow(true) // ๋ฐฐํฐ๋ฆฌ ๋ถ์กฑ์ํ๊ฐ ์๋ ๋๋ง ์๋ํ๋ค.
.build()
val workRequest = OneTimeWorkRequestBuilder<SomeWorker>()
.setConstraints(constraints)
.build()
๐ ํ๊ทธ ์ถ๊ฐ ๋ฐ ์์ ๊ด๋ฆฌํ๊ธฐ
๋ชจ๋ ์์ ์์ฒญ์ ๊ณ ์ ์๋ณ์์ธ ํ๊ทธ๋ฅผ ๋ฃ์ด ํด๋น ์์ ์ ์ทจ์ํ๊ฑฐ๋ ์งํ ์ํฉ์ ํ์ธํ ์ ์๋ค.
WorkRequestBuilder์ addTag() ๋ฉ์๋๋ฅผ ํตํด ํ๊ทธ๋ฅผ ์์ฑํ์ฌ ์๋ณํ ์ ์๋๋ก ๋ง๋ค์ด์ค๋ค.
ํ๊ทธ ๊ฐ์ ํตํ์ฌ ์ทจ์๋ ๊ฐ๋จํ๊ฒ ํ ์ ์์ผ๋ฉฐ ์์ ์งํ ์ํฉ์ ๋ฐํ๋๋ WorkInfo ๊ฐ์ฒด๋ฅผ ํตํด ํ์ธํ ์ ์๋ค.
- WorkInfo State
- BLOCKED
- CANCELLED
- ENQUEUED
- FAILED
- RUNNING
- SUCCEEDED
ํด๋น ๊ฐ์ฒด๋ LiveData๋ก ๋ฐํ๋ฐ์ ์ ์์ผ๋ฉฐ, ์ํ์ ๋ฐ๋ผ์ progress๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ฑ ๋ค์ํ ์์ ์ ์ฒ๋ฆฌํ ์์๋ค.
// add tag
val workRequest = OneTimeWorkRequestBuilder<SomeWorker>()
.addTag("some_worker") // Add this method
.build()
// cancel work
workManager.cancelAllWorkByTag("some_worker")
// get work info to livedata
val workInfos = workManager.getWorkInfosByTagLiveData("some_worker")
// observe example in view
workInfos.observe(lifecycleOwner) { workInfos ->
if (listOfWorkInfo.isNullOrEmpty()) return@observe
val workInfo = workInfos[0]
if (workInfo.state.isFinished) { // SUCCEEDED or FAILED or CANCELLED
hideProgressBar()
} else {
showProgressBar()
}
}
๐ ์ฌ๋ฌ ์์ ์ฒด์ด๋ํ๊ธฐ
์์ ์ ์์ฐจ์ ๋๋ ๋์์ ์คํ๋๋๋ก ๋ง๋๋ WorkRequest๋ฅผ ์์ฑํ ์ ์๋ค.
์๋ฅผ ๋ค์ด, ์๋ ๊ทธ๋ฆผ๊ณผ ๊ฐ์ ์ํฉ์ด ์์ ์ ์๋ค.
Work1๊ณผ Work2 ํ ์คํฌ๋ฅผ ๋์์ ์คํํ๊ณ ํด๋น ์์ ์ด ๋๋ ์ดํ์ Work3, Work4๋ฅผ ์์๋๋ก ์ฒ๋ฆฌํ๋ ์ํฉ์ด๋ค.
์ฐ์ ์ ํ ์คํฌ๋ฅผ ์ ์ํ Worker๋ค์ WorkRequest๋ก ๋ง๋ค์ด์ค๋ค.
enqueue() ๋ฉ์๋ ๋ด์ WorkRequest๋ฅผ ๋ณด๋ด๋ ๊ฒ์ด ์๋๋ผ, beginWith() ๋ฉ์๋๋ฅผ ํตํด ์์ํ ์์ ์ ๋๊ธด๋ค. ์ด๋ ๋ ๊ฐ ์ด์์ ์์ ์ ๋์์ ์์ํ๋ ค๋ฉด List์ ๋ด์ ๋๊ฒจ์ค๋ค.
์ ์์ ์ด ๋๋๋ฉด ์์ํ ์์ ๋ค์ then() ๋ฉ์๋๋ฅผ ํตํด ์์ฑํ๋ค. then() ๋ฉ์๋๋ฅผ ํตํด ๊ณ์ ์ฒด์ด๋ํ ์ ์๋ค.
์์ ํ์ ๋ณด๋ผ ํ ์คํฌ๋ฅผ ๋ชจ๋ ์์ฑํ์์ผ๋ฉด, enqueue() ๋ฉ์๋๋ฅผ ํตํด ํ์ ์ต์ข ์ ์ผ๋ก ์ถ๊ฐํ์ฌ ์์ ์ ์คํํ๋ค.
val work1 = OneTimeWorkRequestBuilder<Work1Worker>().build()
// ... val work2 ~
// workManager is WorkManager Instance.
workManager
.beginWith(listOf(work1, work2))
.then(work3)
.then(work4)
.enqueue()
๐ Worker์ ๋ฐ์ดํฐ ์ฃผ๊ณ ๋ฐ๊ธฐ
Key-Value ์์ผ๋ก ์ด๋ฃจ์ด์ง Worker์ Data๋ก ๋ฐ์ดํฐ๋ฅผ ๋ด์ Worker๋ก ๋ณด๋ผ ์ ์์ผ๋ฉฐ ์์ ์ด ์๋ฃ๋ ํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ ์ ์๋ค.
WorkRequest์ setInputData() ๋ฉ์๋๋ฅผ ํตํด Data๋ฅผ ๋ณด๋ผ ์ ์์ผ๋ฉฐ, ์ด๋ Data๋ฅผ ๋ง๋ค ๋์๋ Data.Builder()๋ฅผ ์ฌ์ฉํ๋ค.
Worker ํด๋์ค์ ์์ฑ์์ธ WorkerParameters๋ก ๋ฐ์ดํฐ๋ ์ ๋ฌ๋๋ฉฐ, getInputData()[=inputData in Kotlin] ๋ฉ์๋๋ฅผ ํตํด ์ฝ๊ฒ ๋ฐ์์ฌ ์ ์๋ค.
์์ ์ ์๋ฃ(์ฑ๊ณต)ํ ํ์ Result.success() ๋ฉ์๋์ Data๋ฅผ ์ ๋ฌํ๋ค.
Data๋ฅผ ๋ง๋ค ๋ Data.Builder()๋ฅผ ํธํ๊ฒ ์์ฑํด์ฃผ๋ ํจ์์ธ workDataOf()๋ ์กด์ฌํ๋ฉฐ, ์๋ Worker ํด๋์ค์์ ํ์ธํด๋ณด์.
๋น๋๋ฅผ ํตํ ์์ฑ์ WorkRequest์ Inputํ ๋ ํ์ธ ํ ์ ์๋ค.
Worker์์ ์ ๋ฌ๋ ๋ฐ์ดํฐ๋ WorkInfo์์ ํ์ธํ ์ ์๋ค.
// input data for worker
val inputData = Data.Builder().apply {
putString("secret_key", "genius-dev")
build()
}
val workRequest = OneTimeWorkRequestBuilder<SomeWorker>()
.setInputData(inputData)
.build()
workManager.enqueue(workRequest)
// in Worker
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
val secretKey = inputData.getString("secret_key")
//..
val outputData = workDataOf("public_key" to "android")
Result.success(outputData)
}
}
// in view(activity, fragment) or viewmodel
workInfos.observe(lifecycleOwner) { workInfos ->
if (listOfWorkInfo.isNullOrEmpty()) return@observe
val workInfo = workInfos[0]
if (workInfo.state.isFinished) {
hideProgressBar()
val outputData = workInfo.outputData.getString("public_key") // <<<< HERE
Toast.makeText(this, "worker output data : $outputData", Toast.LENGTH_SHORT).show()
} else {
showProgressBar()
}
}
๐ References
๊ณต์ ๋ฌธ์ - WorkManager๋ก ์์ ์์ฝ
Android Codelabs - WorkManager
๊ณต์ ๋ฌธ์ - ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฒ๋ฆฌ ๊ฐ์ด๋
๊ณต์ ๋ฌธ์ - ์์ ์์ฒญ ์ ์
๋๊ธ