Search
moon
sun
πŸ”ˆ

Flutter iOS Push Notification

생성일
2021/06/15 15:48
νƒœκ·Έ
Flutter
속성

Dependency

Firebase Messaging

Firebase의 Messaging κΈ°λŠ₯을 μ‚¬μš©ν•˜κΈ° μœ„ν•œ pub.
flutter pub add firebase_messaging
Dart
볡사

iOS ν”„λ‘œμ νŠΈ μ„€μ •

β€’
Required: Capability (xcode project setting)
β€’
Required: ν‘Έμ‹œ μΈμ¦μ„œ 생성 (developer apple center)
β€’
Optional: Remote Notification, Background fetch

Apple Developer μ„€μ •

AppID 생성

앱을 μ•±μŠ€ν† μ–΄μ— μΆœμ‹œν•˜κΈ° μœ„ν•΄μ„œ AppID κ°€ ν•„μš”ν•©λ‹ˆλ‹€. 그리고 AppIDλ₯Ό μΆ”κ°€ν•˜λŠ” κ³Όμ •μ—μ„œ ν•΄λ‹Ή μ•±μ˜ Capabilityλ₯Ό μ„€μ •ν•©λ‹ˆλ‹€. iOS ν”„λ‘œμ νŠΈ μ„€μ •μ—μ„œ XCode Project 에 Capabilityλ₯Ό μΆ”κ°€ν–ˆλ˜ κ³Όμ •μ˜ 짝(?) 이라고 보면 λ©λ‹ˆλ‹€. μ—¬κΈ°μ„œ Notification ν•˜λ‚˜λ§Œ 좔가해주도둝 ν•©λ‹ˆλ‹€.

APN Key 생성

FCM 으둜 Notification을 μ‚¬μš©ν•˜λŠ” 방식은 2κ°œκ°€ μžˆμŠ΅λ‹ˆλ‹€. ν‘Έμ‹œ μΈμ¦μ„œ, APN Key.
ν‘Έμ‹œ μΈμ¦μ„œλŠ” 과거에 μ‚¬μš©ν•˜λ˜ λ°©μ‹μœΌλ‘œ 1λ…„λ§ˆλ‹€ κ°±μ‹ ν•΄μ•Όν•˜λŠ” λ²ˆκ±°λ‘œμ›€μ΄ μžˆμŠ΅λ‹ˆλ‹€. μ‹ κ·œ ν”„λ‘œμ νŠΈλ₯Ό μƒμ„±ν•œλ‹€λ©΄ APN Key 방식을 μΆ”μ²œν•©λ‹ˆλ‹€.

Firebase μ„€μ •

μ•Œμ•„λ‘μ„Έμš”
Flutter iOS Push Notification, FCMλΌλŠ” ν‚€μ›Œλ“œ λ“±μœΌλ‘œ iOSμ—μ„œ Push notification μž‘μ—…μ„ ν•˜λŠ” λΈ”λ‘œκ·Έλ“€μ΄ λ§Žμ€λ°, μ „λΆ€ Capabilityμ—μ„œ background fetch와 Remote notification μ˜΅μ…˜μ„ μ„€μ •ν•˜λΌκ³  ν•©λ‹ˆλ‹€..
ν•˜μ§€ λ§ˆμ„Έμš” 😭 Push Notification 만 μΆ”κ°€ν•΄μ£Όμ‹œλ©΄ λ©λ‹ˆλ‹€.
background fetch와 Remote notificationλŠ” μš©λ„ μžμ²΄κ°€ λ‹€λ¦…λ‹ˆλ‹€...! λ‹€λ₯Έ λΈ”λ‘œκ·Έμ—μ„œλŠ” μ™œ μΆ”κ°€ν•˜λΌλŠ”μ§€ λͺ¨λ₯΄κ² μ–΄μš”..
fyi) background fetch λŠ” 앱이 background μƒνƒœμ— μžˆμ„λ•Œ μ•± λ‚΄ 일뢀 κΈ°λŠ₯듀을 μ‹€ν–‰ν•˜κΈ° μœ„ν•΄ ν•„μš”ν•œ κΆŒν•œμ„ λ°›μŠ΅λ‹ˆλ‹€. μŒμ•… μ•±, 유튜브, λ˜λŠ” μ‹œκ°„μ΄ μ˜€λž˜κ±Έλ¦¬λŠ” μž‘μ—…μ΄μ–΄μ„œ background μ—μ„œλ„ μ΄μ–΄μ„œ ν•΄μ•Όν•˜λŠ” μž‘μ—…λ“€μ„ ν•΄μ•Όν•  λ•Œ ν•„μš”ν•©λ‹ˆλ‹€.
remote notification은 push notification μ€‘μ—μ„œ content-available field 값을 true 둜 ν•˜λŠ” 경우λ₯Ό μ˜λ―Έν•©λ‹ˆλ‹€. 앱이 이 pushλ₯Ό 받은 경우, ν™”λ©΄ 상단에 notification이 λœ¨μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λŒ€μ‹  앱이 backgroundμ—μ„œ μ‹€ν–‰λ©λ‹ˆλ‹€. μ‚¬μš©μž λͺ¨λ₯΄κ²Œμš”..! μ•± λ°±κ·ΈλΌμš΄λ“œμ—μ„œ API 호좜 등을 톡해 μ•± 내에 λ³΄μ—¬μ§€λŠ” 컨텐츠듀을 μ—…λ°μ΄νŠΈ ν•©λ‹ˆλ‹€. μ‚¬μš©μžκ°€ 앱을 μΌœμ„œ μƒˆλ‘œμš΄ λ‚΄μš©μ„ λ°›μ•„μ˜€κΈ° 전에 λΉ λ₯΄κ²Œ λ¨Όμ € μ—…λ°μ΄νŠΈ ν•  λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.

Example

main.dart

import 'dart:developer'; import 'package:chai_booster/Constants.dart'; import 'package:chai_booster/Interceptor/authorizationInterceptor.dart'; import 'package:chai_booster/Pages/Loader/Loader.dart'; import 'package:chai_booster/Pages/RootPage.dart'; import 'package:chai_booster/Providers/Auth/AuthProvider.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:http_interceptor/http_client_with_interceptor.dart'; import 'package:provider/provider.dart'; import 'main.mapper.g.dart' show initializeJsonMapper; void main() { Constants.setEnvironment(Environment.PROD); initializeJsonMapper(); WidgetsFlutterBinding.ensureInitialized(); runApp(App()); } class App extends StatefulWidget { _AppState createState() => _AppState(); } class _AppState extends State<App> { bool _initialized = false; bool _error = false; final client = HttpClientWithInterceptor.build( interceptors: [ AuthorizationInterceptor() ], retryPolicy: ExpiredTokenRetryPolicy() ); void initializeFlutterFire() async { try { await Firebase.initializeApp(); if (Constants.IS_DEBUG) { await FirebaseAuth.instance.useEmulator('http://localhost:9099'); } setState(() { _initialized = true; }); } catch(e) { setState(() { log(e.toString()); _error = true; }); } } void initializeNotification() async { // πŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆ μ—¬κΈ°κ°€ 핡심 await Firebase.initializeApp(); FirebaseMessaging messaging = FirebaseMessaging.instance; await messaging.setAutoInitEnabled(true); _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); await messaging.setForegroundNotificationPresentationOptions( alert: true, badge: true, sound: true, ); } void initState() { initializeFlutterFire(); initializeNotification(); super.initState(); } Widget build(BuildContext context) { if(_error || !_initialized) { return MaterialApp( home: Scaffold( body: Center( child: Loader(), ), ) ); } return MaterialApp( home: MultiProvider( providers: [ ChangeNotifierProvider<AuthProvider>(create: (_) => AuthProvider()), ], child: RootPage(), ) ); } }
Dart
볡사

HomePage.dart // Notification Permission μš”μ²­ν•˜κ³ 

mainPage.dart μ—μ„œλ„ notification permission을 μš”μ²­ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λŒ€μ‹ , μ‚¬μš©μžλŠ” 이 앱이 μ–΄λ–€ κ²½μš°μ— μ•Œλ¦Όμ„ μ£ΌλŠ”μ§€λ„ λͺ¨λ₯Έμ±„ μ•Œλ¦Ό κΆŒν•œ 결정을 ν•΄μ•Όν•©λ‹ˆλ‹€. 쒋은 UXκ°€ μ•„λ‹™λ‹ˆλ‹€.
κ·Έλž˜μ„œ μ‚¬μš©μžμ—κ²Œ μ˜λ―ΈμžˆλŠ” 화면인 HomePage.dartμ—μ„œ κΆŒν•œ μš”μ²­μ„ 받도둝 ν–ˆμŠ΅λ‹ˆλ‹€.
import 'dart:convert'; import 'package:chai_booster/Constants.dart'; import 'package:chai_booster/Interceptor/authorizationInterceptor.dart'; import 'package:chai_booster/Pages/Tabs/BrandAlarmSubscriptions/BrandAlarmSettingPage.dart'; import 'package:chai_booster/Pages/Tabs/More/MorePage.dart'; import 'package:chai_booster/Pages/Tabs/Promotions/PromotionsPage.dart'; import 'package:chai_booster/Pages/Tabs/Today/TodayPage.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:http_interceptor/http_client_with_interceptor.dart'; class HomePage extends StatefulWidget { HomePage({ Key key, this.title }) : super(key: key); final String title; _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int _currentIndex = 0; final _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); void _requestPermission() async { await FirebaseMessaging.instance.requestPermission( alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true, ); final client = HttpClientWithInterceptor.build( interceptors: [ AuthorizationInterceptor() ], retryPolicy: ExpiredTokenRetryPolicy() ); FirebaseMessaging.instance.getToken().then((token){ // FCM 토큰을 μ„œλ²„μ— μ €μž₯ πŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆ client.post(Uri.parse(Constants.API + 'booster/v1/fcm-token'), body: jsonEncode({ 'fcmToken': "$token" })); }); FirebaseMessaging.instance.onTokenRefresh.listen((token) { // FCM 토큰을 μ„œλ²„μ— μ €μž₯ πŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆ client.post(Uri.parse(Constants.API + 'booster/v1/fcm-token'), body: jsonEncode({ 'fcmToken': "$token" })); }); } void didChangeDependencies() { super.didChangeDependencies(); _requestPermission(); πŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆπŸ‘ˆ } Widget build(BuildContext context) { return Scaffold( body: IndexedStack( index: _currentIndex, children: [ for (final tabItem in TabNavigationItem.items()) tabItem.page, ], ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, onTap: (int index) => setState(() => _currentIndex = index), items: [ for (final tabItem in TabNavigationItem.items()) BottomNavigationBarItem( icon: tabItem.icon, label: tabItem.title, ) ], unselectedItemColor: Colors.grey, selectedItemColor: Colors.red, unselectedLabelStyle: TextStyle(color: Colors.grey, fontSize: 10), selectedLabelStyle: TextStyle(color: Colors.blueAccent, fontSize: 10), showSelectedLabels: true, showUnselectedLabels: true, ), ); } } class TabNavigationItem { final Widget page; final String title; final Icon icon; TabNavigationItem({ this.page, this.title, this.icon, }); static List<TabNavigationItem> items() { return [ TabNavigationItem( page: TodayPage(), icon: Icon(Icons.home), title: "ν™ˆ", ), TabNavigationItem( page: PromotionPage(), icon: Icon(Icons.money), title: "ν˜œνƒ", ), TabNavigationItem( page:BrandAlarmSettingPage(), icon: Icon(Icons.alarm), title: "μ•Œλ¦Ό", ), TabNavigationItem( page: MorePage(), icon: Icon(Icons.more), title: "더보기", ), ]; }
Dart
볡사

끝. FCM을 μ΄μš©ν•΄ iOS앱에 Push Notification 을 μ „μ†‘ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€.

λ‹€λ₯Έ λΈ”λ‘œκ·Έλ₯Ό 참고해보면 FirebaseMessagingμ—μ„œ getInitialMessage, onOpenAppMessaging... λ“± λΆˆν•„μš”ν•œ λ‚΄μš©λ“€κΉŒμ§€ λͺ¨λ‘ κ΅¬ν˜„ν•΄λ‘μ—ˆλ”λΌκ΅¬μš”. λ¬Όλ‘  μ„œλΉ„μŠ€ 고도화할 λ•Œ ν•„μš”μ— 따라 κ΅¬ν˜„ν•΄μ•Όν•˜λŠ” λΆ€λΆ„λ“€μ΄μ§€λ§Œ λ‹¨μˆœν•˜κ²Œ 앱에 notification을 μ „μ†‘ν•˜λŠ” 것. μ—¬κΈ°κΉŒμ§€κ°€ λμž…λ‹ˆλ‹€..!