[Android] WorkManager + Coroutine으로 매일 특정시간에 Notification 띄우기
Android/Kotlin

[Android] WorkManager + Coroutine으로 매일 특정시간에 Notification 띄우기


매일 오후 12시에 유저에게 '특별한' Notification을 띄울 수 있는 기능을 제공하고 싶었다. 물론 이 방법을 구현하는 데에는 여러 방법이 있을 수 있다. 서버에서 각 유저에게 FCM을 보내는 방법도 있고 (Super Heavy ^^;), 죽지않는 서비스를 구현하여 12시마다 Notification을 띄워주도록 하는 방법도 있을 수 있겠다. 하지만 후자는 구글에서 지속적으로 막으려는 것을 어떻게든 피한 편법에 가깝다. 따라서 구글에서 계속해서 써보라고 제안하고있는 WorkManager을 사용하여 이를 구현하기로 했다.

 

사실 WorkManager는 꼭 한번이라도 써보고 싶어 예전부터 눈독들여 왔었다. 그동안 죽지않는 서비스 등의 편법으로 백그라운드 처리를 해왔기에, 뭔가 Official 하지 않은 방법을 하고있다는 집착..이 있었기 때문이다.

 

그래서 간만에 공식문서를 보니 그새 Coroutine을 이용한 WorkManager이 나와주셨다.

무려 이름도 CoroutineWorker.. 

가슴이 웅장해진다

 

Coroutine을 WorkManager 상에서 편하게 이용할 수 있도록 코드가 구현되어 있는 WorkManager이다.

이를 아래와 같이 이용할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
class ExampleCoroutineWorker(context: Context, params: WorkerParameters) :
    CoroutineWorker(context, params) {
 
    override suspend fun doWork(): Result = coroutineScope {
                withContext(Dispatchers.IO) {
                //TODO    
        }
 
        Result.success()
    }
}
cs

 

나의 경우 WorkManger을 통해 서버와 통신하고, Response를 바탕으로 Notification을 띄워줘야했기 때문에 네트워크 처리가 필수적이었다. 따라서 Coroutine을 통해서 네트워크 처리를 하면 좋겠다고 생각하고 있었기 때문에 위의 CoroutineWorker을 사용하기로 하였다.

내부 로직의 구체적인 구현은 뒤로하고, 위와 같은 깡통 Worker을 만들었다면, 이를 앱에 어떻게 적용시킬 수 있는지부터 알아보자.

우선 등록할 시점은 개발자 자유지만, 나는 앱을 첫 시동할 때 적용시키기 위해 Application Class를 상속한 곳에서 적용시키기로 하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
class GlobalApplication : Application() {
 
    override fun onCreate() {
        super.onCreate()
 
        initWorkManager(
    }
 
    private fun initWorkManager() {
            //TODO Register WorkManager
    }
}

 

initWorkManager 내에서 "언제, 위의 ExampleCoroutineWorker을 작동시킬 것 인가"를 등록시킬 것이다.

우리는 이제 여기다가 특정 시간마다 (나의 경우 12시 10분) Worker을 작동시켜달라고 요청하는 로직을 구현하면 된다.

 

하지만 그전에 슬픈 사실.. 우리는 WorkManager에게 "몇 날 몇 시에 Worker을 작동시켜줘" 라고 하고 싶지만, 사실 WorkManager에게는 그런 특정한 시간을 지정해줄 수 없다.

 

기본적으로 지원하는 것은 특정 조건 하에서의 단 한 번의 Worker 호출(OneTimeWorkRequest), 혹은 몇 분, 시간 간격으로 반복되는 Worker 호출만이 가능하다(PeriodicWorkRequest). 그래서 우리가 원하는, 특정 시간에서의 Worker 작동을 위해서는 아래와 같은 수를 써야한다.

 

OneTimeWorkRequest을 통해서 ExampleCoroutineWorker을 호출하는 요청을 한다. 그리고
ExampleCoroutineWorker를 통해서 자기 자신(ExampleCoroutineWorker)을 호출하는 OneTimeWorkRequest를 요청한다.

이렇게 하면 무한 반복되는 Work 요청이 가능하다. 이해가 잘 되지 않았다면 아래의 코드를 확인하자.

 

1
2
3
4
5
6
7
8
9
10
//GlobalApplication.kt
private fun initWorkManager() {
        val dailyWorkRequeset = OneTimeWorkRequestBuilder<ExampleCoroutineWorker>()
        .setInitialDelay(getTimeUsingInWorkRequest(), TimeUnit.MILLISECONDS)
        .addTag("notify_day_by_day")
        .build()
 
 
    WorkManager.getInstance(this).enqueue(dailyWorkRequeset)
}

 

위의 GlobalApplication initWorkManager 함수를 마저 구성한 것이다.

setInitalDelay 함수 첫번째 파라미터로 전달된 getTimeUsingInWorkRequest() 함수는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//DateTimeUtils.kt
fun getTimeUsingInWorkRequest() : Long {
        val currentDate = Calendar.getInstance()
        val dueDate = Calendar.getInstance()
 
        dueDate.set(Calendar.HOUR_OF_DAY, 0)
        dueDate.set(Calendar.MINUTE, 10)
        dueDate.set(Calendar.SECOND, 0)
 
        if(dueDate.before(currentDate)) {
                dueDate.add(Calendar.HOUR_OF_DAY, 24)
        }
 
        return dueDate.timeInMillis - currentDate.timeInMillis
}

 

이는 OneTimeWorkRequest를 등록할 때마다, 해당 함수의 리턴값만큼 대기한 이후부터 시작하도록 하기 위해서 사용되는 함수이다.

결과적으로 위의 로직으로써 현재 날짜와 목표 날짜(Worker가 호출되기 바라는 특정 날짜)를 계산하여, 그 차이만큼 대기하였다가 특정 날짜에 Worker가 호출되는 결과를 낳는다.

 

이제 위처럼 Application Class에 WorkRequest를 등록하였으니, 처음으로 Worker을 호출하는 데 까지는 성공한 것이다. 또한 Worker을 아래와 같이 구성한다면, 무한히 반복되는 WorkManager를 구성할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class ExampleCoroutineWorker(context: Context, params: WorkerParameters) :
    CoroutineWorker(context, params) {
 
    private val coroutineExceptionHanlder = CoroutineExceptionHandler { _, throwable ->
        Crashlytics.logException(throwable)
        throwable.printStackTrace()
    }
 
    private val ioDispatchers = Dispatchers.IO + coroutineExceptionHanlder
 
    override suspend fun doWork(): Result = coroutineScope {
 
        /**
         * 반복하여 고정 시간에 WorkRequest를 생성하는 로직 (오전 00시 10분)
         */
        val dailyWorkRequeset = OneTimeWorkRequestBuilder<ExampleCoroutineWorker>()
            .setInitialDelay(getTimeUsingInWorkRequest(), TimeUnit.MILLISECONDS)
            .addTag("notify_day_by_day")
            .build()
 
        WorkManager.getInstance(applicationContext).enqueue(dailyWorkRequeset)
 
        /**
         * 서버에서 목록 가져옴
         */
        val response = withContext(ioDispatchers) {
            //...네트워크 처리...
        }
 
        response?.run {
            //받아온 응답 처리 (Notification 처리 등)
        } ?: run {
            //TODO  아무 일도 없을 때
            Log.d("아무 일도..""없어..")
        }
 
        Result.success()
    }
}

 

쓸 일은 많지 않겠지만 WorkManager을 통해서 이것 저것 해볼 수 있을 것 같다..

 

Reference


https://stackoverflow.com/questions/51612274/check-if-workmanager-is-scheduled-already - Duplicate WorkManager Register 문제 처리

https://medium.com/@joongwon/jetpack-android-background는-workmanager에게-맡기세요-5f6d97331ff3 - PeriodWorkManager의 등록 방법

https://medium.com/androiddevelopers/workmanager-periodicity-ff35185ff006 - 매일 특정 시간에 WorkerManager 반복하는 방법

https://thomass.tistory.com/20 - jvm 1.8 문제