2가지 알림 방식을 구현한다:
flutter pub add flutter_local_notifications
flutter pub add firebase_messaging
flutter pub add firebase_core
flutter pub add timezone
flutter pub add shared_preferences
ios/Runner.xcworkspace 열기import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final plugin = FlutterLocalNotificationsPlugin();
// 초기화 (권한 요청 안함)
Future<void> init() async {
const iOS = DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
final settings = InitializationSettings(iOS: iOS);
await plugin.initialize(settings);
}
// 권한 요청 (지연 요청)
Future<void> requestPermission() async {
await plugin
.resolvePlatformSpecificImplementation<
DarwinFlutterLocalNotificationsPlugin>()
?.requestPermissions(alert: true, badge: true, sound: true);
}
// 예약 알림
Future<void> scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledTime,
String? payload,
}) async {
if (scheduledTime.isBefore(DateTime.now())) {
return;
}
const iOS = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const details = NotificationDetails(iOS: iOS);
await plugin.zonedSchedule(
id,
title,
body,
tz.TZDateTime.from(scheduledTime, tz.local),
details,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
payload: payload,
);
}
// 즉시 알림
Future<void> showNotification({
required int id,
required String title,
required String body,
String? payload,
}) async {
const iOS = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const details = NotificationDetails(iOS: iOS);
await plugin.show(id, title, body, details, payload: payload);
}
// 알림 취소
Future<void> cancelNotification(int id) async {
await plugin.cancel(id);
}
// 모든 알림 취소
Future<void> cancelAll() async {
await plugin.cancelAll();
}
}
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:riverpod/riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
class FirebaseNotificationController extends StateNotifier<Set<String>> {
FirebaseNotificationController() : super({});
// 토픽 구독
Future<void> subscribeToTopic(String topicName) async {
try {
await FirebaseMessaging.instance.subscribeToTopic(topicName);
final prefs = await SharedPreferences.getInstance();
final subscribedTopics = prefs.getStringList('subscribed_topics') ?? [];
if (!subscribedTopics.contains(topicName)) {
subscribedTopics.add(topicName);
await prefs.setStringList('subscribed_topics', subscribedTopics);
}
state = {...state, topicName};
print('토픽 구독 성공: $topicName');
} catch (e) {
print('토픽 구독 실패: $e');
}
}
// 토픽 구독 해제
Future<void> unsubscribeFromTopic(String topicName) async {
try {
await FirebaseMessaging.instance.unsubscribeFromTopic(topicName);
final prefs = await SharedPreferences.getInstance();
final subscribedTopics = prefs.getStringList('subscribed_topics') ?? [];
subscribedTopics.remove(topicName);
await prefs.setStringList('subscribed_topics', subscribedTopics);
state = state.where((topic) => topic != topicName).toSet();
print('토픽 구독 해제: $topicName');
} catch (e) {
print('토픽 구독 해제 실패: $e');
}
}
// 저장된 구독 목록 로드
Future<void> loadSubscribedTopics() async {
final prefs = await SharedPreferences.getInstance();
final subscribedTopics = prefs.getStringList('subscribed_topics') ?? [];
state = subscribedTopics.toSet();
}
}
final firebaseNotificationProvider = StateNotifierProvider<
FirebaseNotificationController,
Set<String>>((ref) => FirebaseNotificationController());
('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
print('백그라운드 메시지 수신');
print('제목: ${message.notification?.title}');
print('내용: ${message.notification?.body}');
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Firebase 초기화
await Firebase.initializeApp();
// NotificationService 초기화
await NotificationService().init();
// FCM 백그라운드 핸들러 등록
FirebaseMessaging.onBackgroundMessage(
firebaseMessagingBackgroundHandler,
);
// FCM 포그라운드 리스너
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('포그라운드 메시지 수신');
print('제목: ${message.notification?.title}');
print('내용: ${message.notification?.body}');
});
runApp(const MyApp());
}
await NotificationService().showNotification(
id: 1,
title: '알림 제목',
body: '알림 본문',
);
final notificationTime = DateTime.now().add(const Duration(minutes: 30));
await NotificationService().scheduleNotification(
id: 2,
title: '예약 알림',
body: '30분 후 나타납니다',
scheduledTime: notificationTime,
);
await ref
.read(firebaseNotificationProvider.notifier)
.subscribeToTopic('topic_name');
await ref
.read(firebaseNotificationProvider.notifier)
.unsubscribeFromTopic('topic_name');
class TopicSubscribeButton extends ConsumerWidget {
final String topicName;
const TopicSubscribeButton({required this.topicName});
Widget build(BuildContext context, WidgetRef ref) {
final subscribedTopics = ref.watch(firebaseNotificationProvider);
final isSubscribed = subscribedTopics.contains(topicName);
return ElevatedButton(
onPressed: () async {
if (isSubscribed) {
await ref
.read(firebaseNotificationProvider.notifier)
.unsubscribeFromTopic(topicName);
} else {
await ref
.read(firebaseNotificationProvider.notifier)
.subscribeToTopic(topicName);
}
},
child: Text(isSubscribed ? '구독 중' : '구독'),
);
}
}
| 항목 | 로컬 알림 | FCM 알림 |
|---|---|---|
| 서버 필요 | 불필요 | 필요 |
| 예약 | 가능 | 불가능 |
| 권한 요청 | 필요 | 필요 |
| 포그라운드/백그라운드 | 모두 지원 | 모두 지원 |
로컬 알림은 기기 내에서 독립적으로 동작하고,
FCM은 서버에서 메시지를 전송할 때 사용한다.
두 방식을 함께 사용하면 효과적인 알림 시스템을 구축할 수 있다.