구현하고자 하는 기능
Task에 설정된 날짜, 시간에 맞춰 Notification을 띄워주는 기능.
조금씩 업그레이드하며 개발하기
- RecyclerView의 Item을 클릭했을 때 -> Notification 발행하기.
- Task를 저장하거나 수정했을 때 -> Alarm 등록하기.
- 등록된 알람은 BroadcastReceiver가 받아서 백그라운드 작업으로 Notification 발행하기.
필요한 기능의 단위를 쪼개서 개발해나간다.
Notificatino 발행 기능
Notification이란 안드로이드 운영체제에서 유저에게 발행하는 알림이다.
노티피케이션을 내보내기 위해서는 시스템으로부터 NOTIFICATION_SERVICE를 빌려와서 실행해야한다.
즉, 만들어진 Notification 객체를 OS에게 보내어 작업을 요청하는 형태로 작동한다.
// Notification 기능을 하나의 클래스로 분리한다.
public class NotificationCreator {
public NotificationCreator(Context context, String channelName, String, channelId) {
this.context = context;
createChannel(channelId, channelName);
}
public void notify(Task task) {
Notification notification = createNotification(task);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.notify("TAG", notificationId, notification);
}
}
NotificationCreator
라는 클래스를 따로 만들어서 Notification에 관한 기능을 맡긴다.
시스템에 작업을 의뢰하기 위해서 ApplicationContext가 반드시 필요하다.
1번의 요구사항을 구현하기 위해서는 RecyclerView.Item의 onClickListener에서 NotificationCreator를 사용해서 notifty()
해주면 된다.
알람 시간 설정하기
지정된 시간에 Reminder 기능을 실행시키려면 예약된 시간에 작업이 수행될 수 있도록 알람을 등록해야한다.
백그라운드 작업을 위해서는 WorkManager와 AlarmManager 같은 클래스의 도움을 받을 수 있다.
두 가지를 비교해본 결과 다음과 같은 특징을 가지고 있다.
* WorkManager
- 앱이 종료되거나 디바이스가 재부팅이 되어도 작업 실행을 보장함.
- 하지만 작업이 실행되는 시점은 정확하게 보장할 수 없음.
- 이는 배터리절전에 친화적인 라이브러리로 디바이스 상태에 따라 OS가 작업을 미룰 수 있다.
- 하지만 호환성에 있어서 장점을 갖는다.
* AlarmManager
- 등록된 작업을 정확한 시간에 실행되어짐을 보장할 수 있다.
- 디바이스 재부팅시 시스템에 등록된 알람은 전부 초기화된다.
Reminder기능은 정확한 시간에 알림이 와야하므로 AlarmManager만이 사용할 수 있는 유일한 옵션이다.
하지만 먼 미래의 작업들은 AlarmManager에 등록시켜놓는 것이 비효율적이며, 디바이스 재부팅과 같은 상태에 따라 등록정보를 잃어버릴 수도 있다.
예를들어 1년 뒤에 Reminder할 내용을 System에 등록시켜 놓는다면, 분명히 1년 뒤까지 system이 재부팅없이 유지되지 않을 확률이 높다. 이를 방지하기 위해서 추후 WorkManager를 활용해서 보완방법을 마련해보도록 하자.
위의 노티피케이션과 마찬가지로 알람매니저도 시스템에서 ALARM_SERVICE를 빌려와서 작업을 의뢰하는 형태이다.
우리가 할 일은 PendingIntent를 생성하고 알람매니저에게 정해진 시간에 PendingIntent의 실행을 의뢰하는 것이다.
public class AlarmUtils {
Context context;
AlarmManager alarmManager;
public AlarmUtils(Context context) {
this.context = context;
alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
}
// ...
public void registerAlarm(Task task) {
PendingIntent pendingIntent = makePendingIntent(task);
long delay; // 알람이 실행될 지연시간을 등록해야한다.
alarmManager.setExact(AlarmManager.RTC_WAKEUP, delay, pendingIntent);
/*
RTC_WAKEUP은 리얼타임을 기준으로 시간을 계산하고, 디바이스가 idle인 상태라도 강제로 WAKEUP시킨다는 의미이다.
setExact는 정확히 예약된 시간에 실행을 보장한다. 배터리 관리에 친화적인 조금 더 느슨한 실행조건을 가진 메소드들도 존재한다.
*/
}
}
이제 PendingIntent가 시스템 알람매니저에 등록되고, setExact() 메소드에서 정한 딜레이 시간 후에 실행될 것이다.
여기서 PendingIntent의 내용물은 어떤 것이 되어야할지 생각해보자.
우리가 구현할 기능은 Notification을 실행하는 것이다. 백그라운드에서 Notification 실행을 시켜줄 누군가가 필요하다.
이 역할을 하는 것이 BroadcastReceiver이다. PendingIntent.getBroadcast()
로 방송을 만들어서 정해진 시간에 방송을 내보낸다.
이 방송을 캐치해서 정해진 백그라운드 작업이 수행되도록 하는 것이다. 물론 이 경우엔 백그라운드에서 Notification을 발행할 것이다.
// 방송을 캐치해서 노티피케이션을 발행할 브로드캐스트 리시버
// AndroidManifest.xml에 등록하는 것을 잊지말자.
public class RegisteredAlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
/*
백그라운드에서 실행될 작업을 여기에서 구현한다.
작업에 필요한 정보는 intent를 사용해서 넘겨받는다.
*/
}
}
구글 문서를 참고하면 브로드캐스트 리시버를 사용할 때 Intent에 너무 큰 정보를 넘겨주지 말것을 권장하고 있다.
예를 들어 Object(Task 전체)를 첨부해서 전달하는 것보다는 key(Task ID)를 전달하고, 리시버 내에서 Key를 통해서 value값을 얻어서 사용하는 형태를 추천한다.
인텐트 자체가 무거워지면 방송으로 전달해야하는 정보량이 늘어나고 시스템에서 전달해야할 작업이 많아지기 때문이다. 실제로 단순 key값이 아니라 object를 넘기려면 Serialize나 Parcelable를 사용해 직렬화를 해야하는데. 오브젝트를 넘길때마다 이 과정을 거치면 속도가 많이 느려질 수 있다.
BroadcastReceiver에서 DB사용하기
Intent에 TASK_ID를 첨부했으니, TASK_ID로 Task를 꺼내오려면 Database를 사용해야 한다.
그렇다면 ViewModel 인스턴스를 사용할 것인가, TaskRepository 인스턴스를 사용할 것인가?
이 경우에는 ViewModel 인스턴스를 넘겨받아서 사용할 수 없다.
왜냐하면 ViewModel의 생명주기가 끝난 상태에서도 BroadcastReceiver는 동작해야하기 때문이다.
현재 Dependency Graph에서 ViewModel은 ActivityScope에 속하는데, 리시버는 애플리케이션이 실행되지 않아도 (Activity 화면이 실행되지 않아도) 작동해야하는 기능이다.
즉 ViewModel보다 상위 그래프에 추가되어야하므로 Application에서 BroadcastReceiver 인스턴스를 관리하고 객체 주입을 해주어야한다.
위 그림과 같이 BroadcastReceiverModule을 AppComponent에 새롭게 추가시킨다.
이제 BroadcastReceiver도 DI시스템의 일원이 되었다.
Dagger는 RegisteredAlarmReceiver에 필요한 객체(TaskRepository)를 주입시켜줄 수 있다.
실제 BroadcastReceiver에 객체 주입하기.
// BroadcastReceiverModule에서는 @ContributesAndroidInjector를 사용한다.
// Activity나 Fragment를 추가한 것과 유사하다.
abstract class BroadcastReceiverModule {
@ContributesAndroidInjector
abstract RegisteredAlarmReceiver contributesRegisteredAlarmReceiver();
}
// 현재 Dagger-android를 사용하고 있으므로
// DaggerBroadcastReceiver를 상속받아서 간편하게 DI에 추가시킬 수 있다.
public class RegisteredAlarmReceiver extends DaagerBroadcastReceiver {
/*
이제 DB에 접근하는 열쇠 객체인 TaskRepository를 주입받아서 사용할 수 있다.
*/
@Inject
TaskRepository taskRepository;
}
Receiver에서 Notification 발행하기
// RegisteredAlarmReceiver.java
@Override
public void onReceive(Context context, Intent intent) {
String taskID = intent.getStringExtra(TASK_ID);
Task task = taskRepository.getTask(taskID);
if (task != null) {
NotificationCreator notiCreator = new NotificationCreator(context, CH_NAME, CH_ID);
notiCreator.notifty(task);
}
}
이제 리시버 내에서 Database에 접근할 수 있기 때문에 TASK_ID값으로 TASK 정보를 가져올 수 있다.
앞서 Notification 발행에 관한 기능을 구현해놓은 NotificationCreator를 사용해서 리시버가 백그라운드 작업으로 노티피케이션이 실행될 수 있도록 구현할 수 있다.
Summary
마지막에 NotificationCreator도 Dagger 시스템에서 객체를 관리하도록 해야하는지 고민해봤다.
하지만 BroadcastReceiver에서 짧은 백그라운드 수행 후 반환될 것이므로 굳이 추가시키지 않고 new로 생성해서 사용하는 것이 좋다고 판단했다.
그리고 노티피케이션의 채널이름을 다양하게 활용할 여지도 있으므로 현재 상태를 유지하도록 한다.
안드로이드는 스마트폰에서 지원하는 다양한 기능을 OS에서 처리하는데.
점점 더 배터리효율에 민감하게 정책이 바뀌어나간다는 것을 느꼈다. WorkManager와 같이 배터리효율을 최우선적으로 작동하도록 구현된 라이브러리가 대표적이다. 그리고 백그라운드 작업엔 WorkManager 사용을 적극 권장하고 있다.
다음으로는 Task가 삭제되었을 때 시스템에 등록된 알람도 삭제해주는 부분을 구현하도록 하자.
+ 추가
'Android' 카테고리의 다른 글
WorkManager (0) | 2019.12.12 |
---|---|
Notification에 Content Intent 추가하기 (0) | 2019.12.12 |
Logic 분리하기 (0) | 2019.11.01 |
EditText Scrollable (0) | 2019.10.25 |
자동 Keypad 보여주기 (0) | 2019.10.25 |