Flutter로 멋진 노트 앱을 만들어보자 ⑤ - 노트 알림 로직 구현하기
지난 글에서는 노트 알림 UI를 구현했었습니다. 이번 글에서는 이어서 노트 알림 로직을 구현해 보겠습니다.
제가 만드는 노트 알림 기능은 크게 두 가지, 예약 알림과 반복 알림을 지원합니다. 사용자는 각각의 알림을 활용하여 자신이 작성한 노트를 원하는 시간에, 정기적으로 받아 볼 수 있습니다.
구현 순서는 다음 목차 순서대로 진행합니다.
목차
- 필요한 패키지 설치
- Notification 모델 정의
- 알림 저장소 구현
- 알림 서비스 구현
1. 주요 패키지 설치
알림 로직을 구현하기 위해 필요한 패키지는 다음과 같습니다.
1. flutter_local_notifications: 로컬 알림을 처리하기 위한 주요 패키지입니다.
2. timezone: 알림 스케줄링을 위한 시간대 관리에 사용됩니다.
아래 명령어를 입력하여 패키지를 설치합니다.
flutter pub add flutter_local_notifications timezone
2. Notification 모델 정의
알림 정보를 관리하기 위해 NoteNotification 모델을 정의했습니다. 이 모델은 알림 유형(예약 또는 반복), 알림 내용, 생성 시간, 활성화 상태 등의 데이터를 포함합니다.
import 'package:flutter/material.dart';
import 'package:ttingnote/enums.dart';
class NoteNotification {
final String id;
final String noteId;
final String content;
final NotificationType type;
final DateTime? scheduledDateTime;
final List<int>? weekdays;
final TimeOfDay timeOfDay;
final DateTime createdAt;
bool isActive;
NoteNotification({
String? id,
required this.noteId,
required this.content,
required this.type,
this.scheduledDateTime,
this.weekdays,
required this.timeOfDay,
DateTime? createdAt,
this.isActive = true,
}) : this.id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
this.createdAt = createdAt ?? DateTime.now();
Map<String, dynamic> toJson() => {
'id': id,
'noteId': noteId,
'content': content,
'type': type.name,
'scheduledDateTime': scheduledDateTime?.toIso8601String(),
'weekdays': weekdays,
'hour': timeOfDay.hour,
'minute': timeOfDay.minute,
'isActive': isActive,
'createdAt': createdAt.toIso8601String(),
};
factory NoteNotification.fromJson(Map<String, dynamic> json) {
return NoteNotification(
id: json['id'],
noteId: json['noteId'],
content: json['content'],
type: NotificationType.values.byName(json['type']),
scheduledDateTime: json['scheduledDateTime'] != null
? DateTime.parse(json['scheduledDateTime'])
: null,
weekdays:
json['weekdays'] != null ? List<int>.from(json['weekdays']) : null,
timeOfDay: TimeOfDay(
hour: json['hour'],
minute: json['minute'],
),
isActive: json['isActive'],
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'])
: DateTime.now(),
);
}
NoteNotification copyWith({
String? id,
String? noteId,
String? content,
NotificationType? type,
DateTime? scheduledDateTime,
List<int>? weekdays,
TimeOfDay? timeOfDay,
DateTime? createdAt,
bool? isActive,
}) {
return NoteNotification(
id: id ?? this.id,
noteId: noteId ?? this.noteId,
content: content ?? this.content,
type: type ?? this.type,
scheduledDateTime: scheduledDateTime ?? this.scheduledDateTime,
weekdays: weekdays ?? this.weekdays,
timeOfDay: timeOfDay ?? this.timeOfDay,
createdAt: createdAt ?? this.createdAt,
isActive: isActive ?? this.isActive,
);
}
}
toJson 은 객체를 map 형태로 반환할 때 사용하고, copyWith 은 원본 객체를 수정하지 않고, 새로운 객체가 필요하거나 일부 값만 변경하고 싶을 때 사용합니다.
3. 알림 저장소 구현
NotificationRepository는 SharedPreferences를 활용하여 알림 데이터를 로컬에 저장하거나 불러옵니다.
class NotificationRepository {
static const String _notificationsKey = 'note_notifications';
final SharedPreferences _prefs;
NotificationRepository(this._prefs);
Future<Map<String, List<NoteNotification>>> loadNotifications() async {
final String? jsonString = _prefs.getString(_notificationsKey);
if (jsonString == null) return {};
final Map<String, dynamic> jsonMap = json.decode(jsonString);
return jsonMap.map((key, value) => MapEntry(
key,
(value as List).map((item) => NoteNotification.fromJson(item)).toList(),
));
}
Future<void> saveNotifications(
Map<String, List<NoteNotification>> notifications) async {
final jsonString = json.encode(
notifications.map((key, value) => MapEntry(
key,
value.map((n) => n.toJson()).toList(),
)));
await _prefs.setString(_notificationsKey, jsonString);
}
}
4. 알림 서비스 구현
알림 설정과 관리의 핵심은 NotificationService입니다. 알림 등록, 취소, 복원, 활성화/비활성화 등의 모든 작업을 이 클래스에서 처리합니다.
아래처럼 NotificationService 클래스를 선언하겠습니다. UI가 변경 사항을 구독할 수 있도록 ChangeNotifier 를 상속합니다.
class NotificationService extends ChangeNotifier {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final FlutterLocalNotificationsPlugin _notifications =
FlutterLocalNotificationsPlugin();
// 노트별 알림 목록을 저장할 Map
final Map<String, List<NoteNotification>> _noteNotifications = {};
late final NotificationRepository _repository;
...
초기화 메서드
알림 초기화 시 flutter_local_notifications와 timezone을 설정하고 저장된 알림 데이터를 가져옵니다.
Future<void> initialize() async {
tz.initializeTimeZones();
final prefs = await SharedPreferences.getInstance();
_repository = NotificationRepository(prefs);
// 알림 데이터 조회
final savedNotifications = await _repository.loadNotifications();
_noteNotifications.addAll(savedNotifications);
// 각 플랫폼 초기화 설정
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
final iosSettings = DarwinInitializationSettings(
requestAlertPermission: true, // 사용자에게 알림 배너 표시 권한 요청
requestBadgePermission: true, // 앱 아이콘 배지 표시 권한 요청
requestSoundPermission: true, // 알림 소리 재생 권한 요청
);
final initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _notifications.initialize(initSettings);
}
앱 시작 시 저장된 알림 복원
저장된 알림 데이터를 불러오고 상태를 확인한 다음 재스케줄링합니다. 사용자의 알림을 동기화함으로써 안정적인 알림 서비스를 제공하기 위한 과정입니다.
Future<void> restoreNotifications() async {
// 1. 저장된 알림 불러오기
final savedNotifications = await _repository.loadNotifications();
final now = DateTime.now();
for (final entry in savedNotifications.entries) {
for (final notification in entry.value) {
// 2. 비활성 알림은 스킵
if (!notification.isActive) continue;
// 3. 예약 알림이고 이미 지난 시간이면 비활성화
if (notification.type == NotificationType.scheduled &&
notification.scheduledDateTime != null) {
if (notification.scheduledDateTime!.isBefore(now)) {
final updatedNotification = notification.copyWith(isActive: false);
_updateNotificationInMemory(updatedNotification);
await _repository.saveNotifications(_noteNotifications);
continue;
}
}
try {
// 4. 알림 다시 스케줄링
await scheduleNotification(notification);
} catch (e) {
print('Failed to restore notification: $e');
// 5. 스케줄링 실패 시 비활성화
final updatedNotification = notification.copyWith(isActive: false);
_updateNotificationInMemory(updatedNotification);
await _repository.saveNotifications(_noteNotifications);
}
}
}
// 6. 메모리에 최종 상태 저장
_noteNotifications.clear();
_noteNotifications.addAll(savedNotifications);
notifyListeners();
}
알림 초기화와 복원 메서드는 main 함수에서 호출하도록 추가해 주세요.
void main() async {
...
// NotificationService 로직
final notificationService = NotificationService();
await notificationService.initialize(); // 초기화
await notificationService.restoreNotifications(); // 저장된 알림 복원
...
runApp(MyApp(notificationService: notificationService));
알림 등록 및 취소
알림 유형에 따라 예약 또는 반복 알림을 설정하며, 알림 취소 시 해당 ID로 등록된 모든 알림을 제거합니다.
Future<void> scheduleNotification(NoteNotification notification) async {
if (!notification.isActive) return;
switch (notification.type) {
case NotificationType.scheduled:
await _scheduleOneTimeNotification(notification);
break;
case NotificationType.recurring:
await _scheduleRecurringNotification(notification);
break;
}
}
Future<void> cancelNotification(NoteNotification notification) async {
await _notifications.cancel(notification.noteId.hashCode);
if (notification.type == NotificationType.recurring) {
for (var weekday in [1, 2, 3, 4, 5, 6, 7]) {
await _notifications.cancel('${notification.noteId}_$weekday'.hashCode);
}
}
}
예약 알림 스케줄링
특정 날짜와 시간을 알림으로 등록합니다.
Future<void> _scheduleOneTimeNotification(
NoteNotification notification,
String content,
) async {
if (notification.scheduledDateTime == null) return;
final scheduledDate = tz.TZDateTime.from(
notification.scheduledDateTime!,
tz.local,
);
await _notifications.zonedSchedule(
notification.noteId.hashCode,
'띵노트 알림',
content,
scheduledDate,
_notificationDetails,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
}
반복 알림 스케줄링
특정 요일과 시간대를 설정하여 반복 알림을 등록합니다.
Future<void> _scheduleRecurringNotification(
NoteNotification notification,
String content,
) async {
if (notification.weekdays == null || notification.weekdays!.isEmpty) return;
// 각 요일별로 알림 설정
for (final weekday in notification.weekdays!) {
final now = DateTime.now();
var scheduledDate = DateTime(
now.year,
now.month,
now.day,
notification.timeOfDay.hour,
notification.timeOfDay.minute,
);
// 다음 해당 요일까지의 날짜 계산
while (scheduledDate.weekday != weekday) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}
// 이미 지난 시간이면 다음 주로 설정
if (scheduledDate.isBefore(now)) {
scheduledDate = scheduledDate.add(const Duration(days: 7));
}
await _notifications.zonedSchedule(
'${notification.noteId}_$weekday'.hashCode,
'띵노트 알림',
content,
tz.TZDateTime.from(scheduledDate, tz.local),
_notificationDetails,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.dayOfWeekAndTime,
);
}
}
마무리
기본적인 노트 알림 기능 로직을 구현해 보았습니다.
이 구현을 통해 사용자는 각 노트에 대해 다음과 같은 기능을 사용할 수 있습니다.
- 예약 알림 설정
- 반복 알림 설정(특정 요일별)
- 알림 데이터를 로컬에 저장 및 복원
이렇게 저장소와 서비스의 역할을 분리하여 구현하면 코드의 변경사항을 최소화할 수 있습니다.
다음 글에서는 알림 기능의 마지막, 알림 기능 로직을 UI와 연결하여 실제 알림이 동작하는지 확인해 보겠습니다.
[Flutter로 멋진 노트 앱을 만들어보자] 시리즈는 직접 독학으로 하나씩 만들어나가는 과정이므로 틀리거나 부족한 내용이 있을 수 있습니다. 조언과 피드백을 댓글로 남겨주시면 적극 반영하겠습니다. 감사합니다.