이전에 WorkManager에 대해 2번 포스팅한 적이 있다.
(지금 다시 읽어보니 꽤괜 포스팅...ㅋㅋ)
사실 그때는 그냥 이런게 있구나 정도였는데,
이번 담당 Feature로 펌웨어를 API를 통해 전송하는 기능을 구현하면서 새롭기도 하고 WorkManager를 좀 더 알게된 느낌이다.
그래서 이번 포스팅은 개발하면서 고민했던 부분, 새로워진 구현부에 대해 작성해보려고 한다.
Service
long-running operations을 background에서 수행하는 Application Component
우리가 흔히 아는 Android의 4대 컴포넌트 중 하나라고 할 수 있다.
background에서 돌기 때문에 UI는 제공하지 않고, 다른 앱으로 전환해도 얼마 동안은 계속 실행된다.
Service는 Foreground, Backgroud, Bind 총 3가지 유형이 있다.
Foreground Service
*Notification을 표시해야 하는 Service → 사용자 눈에 보이는 Service
사용자가 작업이 진행되고 있음을 인지할 수 있도록 Notification은 작업이 중단/종료되기 전까지는 사라질 수 없다.
따라서 작업은 Background에서 진행되지만, 사용자에게 UI를 제공함으로서 Foreground에서 표시하는 Service이다.
Background Service
사용자가 직접 알아채지 못하는 작업
말 그대로 Background에서 진행되는 Serivce를 의미한다.
그렇다면 어떤 Component를 사용해야 할까?
🤔 내 상황은 다음과 같았다.
- 3~15분 걸리며 즉시 시작해야 하는 작업
- 진행률과 성공/실패 결과를 Notification으로 표시해야 함
- 앱을 Background에 두더라도 실행이 되어야 함
일단 무조건 Notification을 띄워야 했기 때문에 Foreground Service를 써야 하나 고민했다.
Foreground Service를 사용하기 위해서는 Service가 아닌 Lifecycle-aware한 LifecycleService를 많이 사용하고 있었다.
하지만 결론적으로는 1번만 즉시 실행되는 WorkManager로 구현하게 되었다.
이유는 아래와 같다.
CoroutineWorker | LifecycleService | |
앱 종료 |
|
|
결과 표시 (Notificaiton) |
|
|
장시간 작업 |
|
|
WorkManager 쉽게 구현하기
- 초기화 하기
- Hilt로 의존성 주입하기
- WorkManager 작성하기
- 상태 관찰하기
비교적 간단한 4가지 단계로 WorkManager를 구현할 수 있다.
1. 초기화 하기
java.lang.IllegalStateException: WorkManager is already initialized.
Application에 WorkManager를 초기화하니 이런 에러가 났다. 처음 선언하는 것인데 이미 초기화가 되었다니?
공식 문서를 다시 읽어보니, WorkManager의 초기화에 대한 3가지 방법이 있었다.
- 앱에서 androidx.startup 라이브러리를 사용하고 있는 경우
<!-- If you want to disable android.startup completely. -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
- androidx.startup 라이브러리를 사용하는 경우
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- If you are using androidx.startup to initialize other components -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
- 2.6 이전의 WorkManager를 사용하는 경우
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
🌐 androidx.startup을 사용한 이유?
WorkManager 내부적으로 2.6.0 버전부터 androidx.startup 라이브러리를 사용하여 초기화해야 함
→ 이전에는 WorkManagerInitializer를 사용해서 초기화하고 있었음
androidx.startup 라이브러리를 간단하게 설명하자면
- android library는 초기화하는 것에 대해 순서를 보장하지 않음
- androidx.startup → android library의 초기화 순서를 정할 수 있음
- 라이브러리마다 사용하는 Content Provider를 초기화하는 비용이 있고, 라이브러리가 많아질 수록 앱 시작 시간이 길어짐
- android.startup → 여러 컴포넌트의 초기화를 하나의 Content Provider를 통해 처리하기 때문에 시간 단축 가능
따라서 앱이 시작할 때 WorkManager를 초기화하는 것이 아니라 해당 컴포넌트가 필요할 때 초기화하고 최적하기 위해 사용한 것.
2. Hilt로 의존성 주입하기
WorkManager 안에서 UseCase, Repository 등을 사용하게 될 경우, Hilt를 사용해야 한다.
이미 hilt를 사용하고 있는 중이라고 가정하고, 아래 hilt-work 라이브러리를 추가한다.
implementation 'androidx.hilt:hilt-work:1.0.0'
@HiltWorker
class TransferWorkManager @AssistedInject constructor(
@Assisted val context: Context,
@Assisted params: WorkerParameters,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
...
}
}
- @HiltWorker : Worker로 사용할 클래스를 정의함
- @AssistedInject : Worker 객체의 생성자를 주입
- @Assisted : 필요한 종속 항목들을 지정
@HiltAndroidApp
class App: Application(), Configuration.Provider {
@Inject
lateinit var transferWorkerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.setWorkerFactory(transferWorkerFactory)
.build()
}
- Hilt에 의해 Worker의 생성자는 HiltWorkerFactory를 통해 생성됨
❇️ HiltWorkerFactory가 아니라 직접 Factory를 만드려면?
class TransferWorkerFactory @Inject constructor(
private val workerParameters: WorkerParameters
): WorkerFactory() {
override fun createWorker(
appContext: Context,
workerParameters: WorkerParameters
): ListenableWorker? {
return when (workerClassName) {
TransferWorkManager::class.java.name -> {
TransferWorkManager(
appContext,
workerParameters
)
}
else -> null
}
}
}
@Module
@InstallIn(SingletonComponent::class)
object WorkerModule {
@Provides
fun provideWorkerFactory(
workerParameters: WorkerParameters
): WorkerFactory {
return TransferWorkerFactory(workerParameters)
}
}
workerParameters에 원하는 UseCase, Repository, Util 클래스 등을 넣을 수 있을 것이다.
3. WorkManager 작성하기
@HiltWorker
class TransferWorkManager @AssistedInject constructor(
@Assisted val context: Context,
@Assisted params: WorkerParameters,
private val localUseCase: LocalUseCase,
private val localFwUseCase: EmbLocalFirmwareUpdateUseCase,
private val networkUtil: NetworkUtil,
) : CoroutineWorker(context, params) {
companion object {
const val WORKER_ID = "transfer_worker"
const val FAIL_MSG = "fail_message"
// input data로 전달받을 value의 key를 선언했음
...
}
override suspend fun doWork(): Result {
// WorkManager가 수행할 작업
...
// 성공
setForeground(createSuccessNotification())
delay(500L)
Result.success()
// 실패
setForeground(createFailedNotification())
delay(500L)
Result.failure(workDataOf(FAIL_MSG to "$errMsg"))
}
}
- WorkManager에는 Request를 Builder로 생성하고 InputData를 넘길 수 있다. (함수의 파라미터와 비슷한 개념)
- 이때 Key-Value형태로 넘기기 때문에 WorkManager 안팎으로 사용하는 Key를 object로 선언해서 사용했다.
- doWork()는 WorkManager가 실제 작동할 로직을 작성한다. (ex. 파일 업로드, 다운로드 등)
fun getTransferWorkRequest(
files: FirmwareFiles,
...
): OneTimeWorkRequest {
val data = Data.Builder()
data.putAll(workDataOf(TransferWorkManager.EMS_FILES to files))
...
return OneTimeWorkRequestBuilder<TransferWorkManager>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
...
.setInputData(data.build())
.build()
}
- WorkManager로 넘기고자 하는 inputData는 기본형만 지원한다.
- 따라서 복잡한 데이터 구조를 넘기고 싶다면 jsonString으로 파싱해서 WorkManager에 보내고, 받은 뒤에 원하는 데이터 구조로 다시 파싱해서 사용하면 좀 더 깔끔하게 사용할 수 있다.
🏃♀️➡️ 신속 처리 작업
Builder에 보면 setExpedited()를 볼 수 있다. WorkManager 2.7부터 지원하는 기능이다.
할당량이 허용한다면 해당 함수를 사용한 WorkManager는 즉시 백그라운드에서 실행된다.
✅ 그래서 이 할당량에 대한 조건을 설정해주어야 한다.
val request = OneTimeWorkRequestBuilder<SyncWorker>()
<b>.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)</b>
.build()
WorkManager.getInstance(context)
.enqueue(request)
- OutOfQuotaPolicy : 실행하는 앱이 execution quota(= 할당량)에 근접했을 때에 대한 정책
- RUN_AS_NON_EXPEDITED_WORK_REQUEST : ordinary work로 실행하게 함 (신속이 아닌 일반 작업으로 처리)
- DROP_WORK_REQUEST : sufficient quota가 아니라면 cancel (취소)
✅ 한가지 더 주의할 점은, Android 12보다 낮은 플랫폼과의 호환성이다.
더 낮은 플랫폼에서는 WorkManager가 Foreground Service를 실행할 수도 있다.
위에도 표로 정리하여 말했지만 Foreground Service는 Notification을 무조건! 호출해야 한다.
- 12 미만 지원 → getForegroundInfoAsync()나 getForegroundInfo()를 사용해서 Notification 띄우기
- 12 이상 지원 → setForeground()를 통해 Foreground Service 사용
- 그렇지 않으면 Runtime Exception이 발생할 수 있음
실제로 setForegound를 작성하지 않고 일반적인 NotificationBuilder를 통해서 알림을 띄워보니, 앱을 백그라운드로 보냈을 때 WorkManager가 런타임 에러로 인해 재시작되는 비정상 동작이 있었다.
4. 상태(WorkInfo) 관찰하기
workerManger는 LiveData를 지원한다.
우리가 LiveData에서 받아볼 수 있는 데이터는 doWork() (WorkManager가 실행하는 작업)
중간 혹은 완료 후 내보낸 Key-Value 형태의 데이터를 WorkManager의 상태와 함께 WorkInfo라는 데이터 구조로 받을 수 있다.
setProgress(workDataOf(Progress to 0))
Result.success(...)
Result.failure(...)
- setProgress를 통해 진행률을 밖에서 관찰할 수도 있음
- workDataOf를 래핑하고 있는 함수가 곧 WorkInfo의 상태
(아래 상태 말고도 ENQUEUED, CANCELLED, BLOCKED가 있음)- setProgress → State.RUNNING
- Result.success → State.SUCCEEDED
- Result.failure → State.FAILED
WorkManager.getInstance(applicationContext)
// requestId is the WorkRequest id
.getWorkInfoByIdLiveData(requestId)
.observe(observer, Observer { workInfo: WorkInfo? ->
if (workInfo != null) {
val progress = workInfo.progress
val value = progress.getInt(Progress, 0)
// Do something with progress information
}
})
- 앞서 말한 것처럼, Key-Value의 Key를 object로 선언한데는 이처럼 밖에서 Key를 이용해 데이터를 가져와야 한다.
- WorkManager의 ID를 통해서 가져올 수도 있고, WorkManager 자체를 Unique하게 만들 수도 있다.
WorkManager를 공부는 했어도 직접 제대로 구현해본 건 처음이라 글을 남겼다.
내 이전 글이 도움이 되기도 하고, WorkManager는 공식문서가 특히나 자세해서 공식문서만으로도 구현이 쉬웠다.
혹시 잘못된 내용이 있다면 누구든 편하게 댓글 남겨주세요!
Reference
맞춤 WorkManager 구성 및 초기화 | Background work | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 맞춤 WorkManager 구성 및 초기화 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 기본적으로 WorkManager는
developer.android.com
다른 Jetpack 라이브러리와 함께 Hilt 사용 | App architecture | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 다른 Jetpack 라이브러리와 함께 Hilt 사용 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Hilt에는 다른 J
developer.android.com
작업 요청 정의 | Background work | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 작업 요청 정의 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 시작 가이드에서는 WorkRequest를 만들고
developer.android.com
WorkInfo | API reference | Android Developers
androidx.appsearch.builtintypes.properties
developer.android.com
중간 worker 진행률 관찰 | Background work | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 중간 worker 진행률 관찰 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. WorkManager에 worker의 중간 진행률
developer.android.com
'ANDROID > Android Jetpack' 카테고리의 다른 글
[Android/Jetpack] AAC - WorkManager (2) (0) | 2022.01.31 |
---|---|
[Android/Jetpack] AAC - WorkManager (Persistent Background) (0) | 2022.01.27 |
[Android/Jetpack] AAC - Paging (0) | 2022.01.19 |
[Android/Jetpack] Recyclerview Adpater 대신 ListAdapter 적용하기 (0) | 2021.09.27 |
[Android/Jetpack] Room + LiveData + ViewModel : 코루틴을 이용한 예제(2) (0) | 2021.09.25 |
[Android/Jetpack] Room + LiveData + ViewModel : 코루틴을 이용한 예제(1) (0) | 2021.09.24 |
[Android/Jetpack] AAC - Data Binding (0) | 2021.08.11 |
[Android/Jetpack] AAC - Room (0) | 2021.08.03 |
댓글