나는 이렇게 논다/Flutter 로 간단한 노트 앱을 만들어보자

Flutter로 멋진 노트 앱을 만들어보자 ⑤ - 노트 알림 로직 구현하기

daco2020 2024. 11. 28. 15:53
반응형

지난 글에서는 노트 알림 UI를 구현했었습니다. 이번 글에서는 이어서 노트 알림 로직을 구현해 보겠습니다.

 

제가 만드는 노트 알림 기능은 크게 두 가지, 예약 알림반복 알림을 지원합니다. 사용자는 각각의 알림을 활용하여 자신이 작성한 노트를 원하는 시간에, 정기적으로 받아 볼 수 있습니다.

 

구현 순서는 다음 목차 순서대로 진행합니다.

목차

  1. 필요한 패키지 설치
  2. Notification 모델 정의
  3. 알림 저장소 구현
  4. 알림 서비스 구현

 

 

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로 멋진 노트 앱을 만들어보자] 시리즈는 직접 독학으로 하나씩 만들어나가는 과정이므로 틀리거나 부족한 내용이 있을 수 있습니다. 조언과 피드백을 댓글로 남겨주시면 적극 반영하겠습니다. 감사합니다.

 

반응형