EP23. Flutter 패키지 Push Notification Service 구현
1. pub 설치
Push Notification 구현을 위해 패키지 수준의 pubspec에 필요한 의존성을 설치합니다.
flutter pub add firebase_messaging
flutter pub add flutter_local_notifications
flutter pub add package_info_plus
flutter pub get
Bash
복사
2. Android 권한 설정
•
app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> // 👈 Notification 권한 사용 선언
<application ...>
XML
복사
Notification 권한 사용을 명시적으로 선언합니다.
Android SDK33 (안드로이드 13) 기준으로 알림 권한이 일부 변경되었습니다.
크게 SDK 26 버전부터 Channel 명시적 설정 필요, SDK 33 버전부터 Notification 명시적 권한 획득 필요.
강의 코드에서는 모든 버전에 호환되도록 코드를 구성합니다.
3. iOS Push Notification 설정
iOS Simulator에서는 Push Notification 기능이 동작하지 않습니다. 실제 디바이스에서의 Push Notification은 애플 개발자 멤버십을 소유한 경우에 사용 가능합니다.
4. PushNotificationService 구현
패키지에 푸시 권한, FCM 토큰 획득, 푸시 수신 등 푸시 기능 전반을 담당할 PushNotificationService를 구현합니다.
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:package_info_plus/package_info_plus.dart';
// 플러그인 인스턴스를 전역으로 정의
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
late AndroidNotificationChannel channel;
const String notificationIcon = '@mipmap/ic_launcher'; // '@mipmap/ic_launcher'파일을 푸시 아이콘으로 사용합니다.
// 최상위 함수로 백그라운드 메시지 핸들러 정의
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// 필요한 초기화 작업 수행
await _initializeFlutterLocalNotificationsPlugin();
await _initializeAndroidNotificationChannel();
var notification = message.notification;
var android = message.notification?.android;
if (notification != null && android != null) {
await flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
channel.id,
channel.name,
icon: notificationIcon,
importance: Importance.high,
),
),
);
}
}
// 플러그인 초기화
Future<void> _initializeFlutterLocalNotificationsPlugin() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings(notificationIcon);
const DarwinInitializationSettings initializationSettingsIOS = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
}
// 안드로이드 알림 채널 초기화
Future<void> _initializeAndroidNotificationChannel() async {
final packageInfo = await PackageInfo.fromPlatform();
final packageName = packageInfo.packageName;
// 안드로이드 알림 채널 설정
channel = AndroidNotificationChannel(
packageName,
'notification',
importance: Importance.high,
);
// 안드로이드용 알림 채널 생성
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
}
class FPPushNotificationService {
FPPushNotificationService() {
_initializeFlutterLocalNotificationsPlugin();
}
// 알림 권한 요청 메서드
Future<void> requestPermission() {
return FirebaseMessaging.instance.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
}
// 현재 알림 권한 상태를 가져오는 메서드
Future<bool?> hasPermission() async {
var settings = await FirebaseMessaging.instance.getNotificationSettings();
switch (settings.authorizationStatus) {
case AuthorizationStatus.authorized:
return true;
case AuthorizationStatus.notDetermined:
// iOS에만 존재하는 상태. iOS는 Push 권한을 나중에 받지만 Android는 Manifest에서 받기 때문에 true/false로만 반환
return null;
default:
return false;
}
}
// 현재 FCM 토큰을 가져오는 메서드
Future<String?> getToken() async {
return await FirebaseMessaging.instance.getToken();
}
// 알림 초기화 메서드
Future<void> initializeNotification({required Function(String? fcmToken) updatedTokenHandler}) async {
FirebaseMessaging messaging = FirebaseMessaging.instance;
await messaging.setForegroundNotificationPresentationOptions(
alert: true, badge: true, sound: true,
);
await _initializeAndroidNotificationChannel();
// 포그라운드 메시지 수신 리스너 설정
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
var notification = message.notification;
var android = message.notification?.android;
if (notification != null && android != null) {
await _showLocalNotification(notification); // 로컬 알림 표시
}
});
// 백그라운드 메시지 핸들러 설정
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// 초기 FCM 토큰 및 토큰 갱신 핸들러 설정
final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
await updatedTokenHandler(token);
}
FirebaseMessaging.instance.onTokenRefresh.listen(updatedTokenHandler);
}
// 로컬 알림 표시 메서드
Future<void> localNotification({int id = 0, required String? title, required String? body}) async {
await flutterLocalNotificationsPlugin.show(
id,
title,
body,
NotificationDetails(
android: AndroidNotificationDetails(
channel.id,
channel.name,
icon: notificationIcon,
importance: Importance.high,
),
),
);
}
// 로컬 알림을 표시하는 내부 메서드
Future<void> _showLocalNotification(RemoteNotification notification) async {
await flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
channel.id,
channel.name,
icon: notificationIcon,
importance: Importance.high,
),
),
);
}
}
Dart
복사
5. RootModel에서 PushNotificationService 활용
import 'package:flutter_package_sample/services/user_service.dart';
import 'package:swm_flutter_package/entities/user.dart';
import 'package:swm_flutter_package/services/push_notification_service.dart';
import 'dart:async';
class RootModel {
final UserService userService;
final FPPushNotificationService pushNotificationService;
RootModel(this.userService, this.pushNotificationService);
Stream<FPUser?> getUserStream() {
return userService.getUserStream().transform(StreamTransformer.fromHandlers(
handleData: (FPUser? user, EventSink<FPUser?> sink) {
if (user != null) {
initPushNotification();
}
sink.add(user);
},
));
}
bool isSignedIn() {
return userService.isSignedIn();
}
void initPushNotification() {
pushNotificationService.initializeNotification(updatedTokenHandler: (fcmToken) async {
if (fcmToken != null) {
await userService.upsertFCMToken(fcmToken: fcmToken);
}
});
}
Future<bool?> hasPermission() async {
return await pushNotificationService.hasPermission();
}
}
Dart
복사
6. RootModel에서 유저 변경시 FCM Token 업데이트 API 호출 확인
RootModel의 getUserStream() 함수에 의해 유저가 변경되면 해당 유저 ID의 FCM Token을 업데이트합니다.
•
FCM Token 업데이트 API
•
Supabase fcm_tokens 테이블에 추가된 FCM Token
7. Push 발송 테스트
•
Firebase Messaging
Firebase Messaging에서 Push Notification을 발송할 수 있습니다.
Push알림의 제목, 내용을 입력 후 ‘테스트 메시지 전송’을 클릭합니다.
Push를 수신할 FCM Token을 입력합니다. FCM Token은 Supabase fcm_tokens테이블에서 찾을 수 있습니다. `테스트`를 클릭하면 해당 FCM Token에 해당하는 디바이스에 푸시 메시지를 전송합니다.