안드로이드

[Android] WorkManager

코딩하는후운 2024. 3. 18. 12:54
반응형

# 공부하게된 이유

FCM 알림을 받던 도중 포그라운드 서비스를 시작하는 이슈가 발생!

 

Android 12 이상을 타겟팅하는 앱은 특별한 사례 몇 가지를 제외하고 백그라운드에서 실행되는 동안 포그라운드 서비스를 시작할 수 없습니다. 

앱이 백그라운드에서 실행되는 동안 WorkManager를 사용하여 신속 처리 작업을 예약하고 시작해 보세요.

신속히 처리해야 하는 사용자 요청 작업을 완료하려면 정확한 알람 내에서 포그라운드 서비스를 시작하세요.

 

앱 성능과 UX를 개선하기 위해 Android 12 이상을 타겟팅하는 앱은 알림 트램펄린으로 사용되는 서비스나 broadcast receiver에서 활동을 시작할 수 없습니다. 즉, 사용자가 알림을 탭하거나 알림 내에서 작업 버튼을 탭한 후, 앱은 서비스나 broadcast receiver 내부에서 startActivity()를 호출할 수 없습니다.
  • 앱이 종료되거나 기기가 다시 시작되어도 실행 예정인 지연 가능한 비동기 작업을 쉽게 예약할 수 있게 해준다
  • 안드로이드의 백그라운드 작업을 처리하는 방법 중 하나, Android Jetpack 아키텍처의 구성 요소 중 하나이다
  • 하나의 코드로 API Level 마다 비슷한 동작을 보장한다

프로세스 종료 여부와 관계없이 반드시 작업을 실행한다.

하지만 작업이 즉시 실행되는 것을 보장하지 않음.

 

# 주요 기능

  • API 14 이상 단말을 지원한다
  • 네트워크 가용성, 충전상태와 같은 작업의 제약 조건을 설정할 수 있다
  • 일회성 혹은 주기적인 비동기 작업을 예약할 수 있다
  • 예약된 작업 모니터링 및 관리
  • 작업 체이닝
  • 앱이나 기기가 다시 시작되는 경우에도 작업 실행을 보장한다
  • 잠자기 모드와 같은 절전 기능을 지원한다
  • WorkManager는 앱이 종료되거나 기기가 다시 시작되는 경우에도 지연 가능 하고 안정적으로 실행되어야 하는 작업을 대상으로 설계되어 있다
    • 백엔드 서비스에 로그 또는 분석을 전송하는 작업
    • 주기적으로 애플리케이션 데이터를 서버와 동기화 하는 작업

 

# WorkManager 구성

  • Worker
    • Abstract class
    • 클래스를 상속받고 백그라운드에서 실행하고자 하는 코드를 doWork() 메소드에 정의한다.
    • 작업 상태를 나타내는 Result에 정의된 success(), failure(), retry() 등의 메소드를 통해 결과를 반환한다.
  • WorkRequest
    • Work에 정의된 Task를 작동시키기 위한 request를 나타낸다.
    • WorkRequest를 생성할 때 반복 여부, 제약 사항 등의 정보를 담는다.
    • OneTimeWorkRequest
      • 한번만 실행할 작업 요청
    • PeriodicWorkRequest
      • 일정 주기로, 여러번 실행할 작업 요청을 나타내는 WorkRequest
  • WorkManager
    • 실제로 WorkRequest를 스케줄링하고 실행하며 관리하는 클래스
    • 인스턴스를 받아와 WorkRequest를 큐에 추가하여 실행하도록 한다.

 

 

# 구현하기

Dependency 추가

dependencies {
    implementation "androidx.work:work-runtime-ktx:2.5.0" // Kotlin + Coroutines
}

 

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작성 및 큐에 작업 추가하기

WorkMager의 인스턴스를 getInstance() 메소드를 통해서 가져오고,

enqueue() 메소드를 통해 WorkRequest를 추가할 수 있다.

 

// in view(activity, fragment) or viewmodel

private val workManager = WorkManager.getInstance(application)

workManager.enqueue(workRequest) // 위에서 작성한 workRequest를 넣어준다.

 

# 더 알아보기

Constraint(제약 사항)

WorkRequestBuilder에 제약 사항을 추가할 수 있다.

Constraints.Builder()를 통해 작성.

  • 네트워크 유형
  • 충전 상태

등등..

https://developer.android.com/topic/libraries/architecture/workmanager/how-to/define-work#work-constraints

 

작업 요청 정의  |  Android 개발자  |  Android Developers

작업 요청 정의 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 시작 가이드에서는 간단한 WorkRequest를 만들고 이를 큐에 추가하는 방법을 살펴보았습니다.

developer.android.com

 

// 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를 생성할 수 있다.

 

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()를 통해 쉽게 받아올 수 있다.

작업을 완료한 후에 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()
    }
}

 

# 실제 적용!

1. WorkManager 클래스 선언

class CallWorkManager(
    val context: Context,
    params: WorkerParameters
) : Worker(context, params) {

    override fun doWork(): Result {
        return try {
            when (inputData.getString(타입 키값)) {
                서비스 종료 키값 -> {
                    context.stopService(
                        Intent(context, Service::class.java)
                    )
                }
                서비스 실행 키값 -> {
                    Intent(context, Service::class.java).apply {
                        action = RingService.ACTION_START_FOREGROUND
                        putExtra(PARAM_DATA, inputData.getString(PARAM_DATA))

                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                            context.startForegroundService(this)
                        } else {
                            context.startService(this)
                        }
                    }
                }
            }

            Result.success()
        } catch (e: Exception) {
            e.printStackTrace()
            Result.failure()
        }
    }

}

 

2. 푸시 왔을 때 실행!

Data inputData = new Data.Builder()
                    .putString(타입 키값, 실행할 타입 값)
                    .build();
                    
WorkRequest workRequest =
                new OneTimeWorkRequest.Builder(CallWorkManager.class)
                        .setInputData(inputData)
                        .build();

WorkManager.getInstance(MyApp.get()).enqueue(workRequest);

 

# 작업하면서 알게 된 점

ForegroundService를 통해 Notification을 띄울 경우

seFullScreenIntent에 의해서 액티비티가 실행된다.(시스템 UI가 전체 화면 Notification을 띄울 건지, Heads-up Notification을 띄울 건지 선택)

https://shwoghk14.blogspot.com/2020/12/android-notification-with-full-screen.html

 

Android Notification with Full Screen

별빛 연구소(StarLight Lab)

shwoghk14.blogspot.com

 

# 액티비티의 화면을 강제로 Screen On 시키고 싶을 경우

액티비티 onCreate에서 사용해 주었다.

private fun turnScreenOnAndKeyguardOff() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
            setShowWhenLocked(true)
            setTurnScreenOn(true)
            window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
        } else {
            window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED    // deprecated api 27
                or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD     // deprecated api 26
                or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON   // deprecated api 27
                or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
        }
        val keyguardMgr = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            keyguardMgr.requestDismissKeyguard(this, null)
        }
    }

 

 

# 안드로이드 12 관련 이슈

  1. 앱 실행중 & FCM에서 startService했을 때 - FCM오는 메시지의 priority를 High를 변경해주니 실행 된듯
  2. FCM받았을 때 앱이 종료 되어있는 경우에는 startActivity를 해서 풀스크린으로 보여주었었다. (서비스 실행으로 바꿈)
    2-1. 앱 종료 & FCM에서 startService했을 때 - 서비스 실행 되지 않음
    : 앱 종료시에도 서비스 실행해줘야하니 WorkManager를 통해 이슈 해결
    2-2. 앱종료시에는 풀스크린으로 보여줘야하는데 이 때 notification에 설정한 setFullScreenIntent에 의해서 액티비티가 실행된다.(시스템 UI가 전체 화면 Notification을 띄울 건지, Heads-up Notification을 띄울 건지 선택)

 

notification을 커스텀하여 remoteview로 노티를 보여주고 있었는데, 커스텀 뷰 클릭시 broadcast로 받아서 startActivity를 해주었는데 Android12에서는 제한하고 있어서 안된다.

 

12미만 : 기존 broadcast를 이용하여 커스텀뷰 & 이벤트 처리

12이상 : broadcast를 이용할 수 없으니 addAction으로 노티피케이션 시스템 버튼 추가하여 intent되도록 작업

 

 

 

 

 

참조 :

https://genius-dev.tistory.com/entry/Android-WorkManager%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%B0%B1%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EC%9E%91%EC%97%85

https://dongsik93.github.io/til/2020/05/15/til-jetpack-workmanager/

반응형