Search
๐Ÿ”ˆ

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์„ ์ „์†กํ•˜๋Š” ๊ฒƒ. ์—ฌ๊ธฐ๊นŒ์ง€๊ฐ€ ๋์ž…๋‹ˆ๋‹ค..!