Using FCM in Flutter
Preface
FCM (Firebase Cloud Message) uses Firebase to control sending and receiving notifications. When I was working on a project, I found there weren’t many articles online sharing how to set it up, and the ones that did exist were quite inconsistent (probably because the firebase_message library updates so often), so I’m recording how the current project does it.
To be honest I still don’t fully understand the why behind everything, so I’m just recording it for now and will fill in more explanation later.
Project Versions
- Flutter: 2.5.3
- firebase_core: ^1.9.0
- firebase_messaging: ^10.0.9
- flutter_local_notifications: ^9.1.5
Process
Initializing the Firebase Project
- First, in the Firebase project, download the SDKs needed for ios/android (GoogleService-Info.plist / google-services.json).
- Make sure the project name matches (e.g. com.expamle.demo).
- For iOS projects, it’s best to use Xcode to import GoogleService-Info.plist. (But iOS also requires some setup on the App Developer side, which I’ll skip here.)
- Download the required libraries in pubspec.yaml.
Android Setup - in {Flutter project}/android/app/build.gradle - (when running you may hit some Android version errors; I made a few modifications too)
//firebase setting
apply plugin: 'com.google.gms.google-services'
android {
// modify 29 to 31 as setting flutter persission
compileSdkVersion 31
defaultConfig {
//modify from 20 to 23 due to firebase services
minSdkVersion 23
}
// ... other setting
}
dependencies {
// ... other dependencies
// add this line
implementation platform('com.google.firebase:firebase-bom:29.0.0')
}
In {Flutter project}/android/build.gradle:
buildscript { dependencies { // ... other dependencies // add this line classpath 'com.google.gms:google-services:4.3.10' } }
iOS Setup
In {Flutter Project}/ios/Runner/AppDelegate.swift:
import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { //firebase setting if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate } //firebase setting end GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }
Flutter Code Setup
In main.dart
Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
}
void main() async {
///Firebase.initializeApp() needs to call native code to initialize Firebase,
///and since this library uses a flutter channel to call native code, that is done asynchronously,
///so WidgetsFlutterBinding.ensureInitialized() must be called to ensure there is a WidgetsBinding instance
WidgetsFlutterBinding.ensureInitialized()
await Firebase.initializeApp();
///allow the device to receive notifications even when the app is not launched
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
/// library: flutter_local_notifications
LocalNotificationService.requestIOSPermissions();
runApp(MyApp())
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
FirebaseNotificationService.initFirebaseNotificaiton(context);
}
}
Add a new file under lib/, firebase_notification_service.dart
class FirebaseNotificationService {
static initFirebaseNotificaiton(BuildContext context, String route) {
/// initialize LocalNotificationService
/// pass context in so we can use Navigator
LocalNotificationService.initialize(context, route);
///give the message which user taps when app is from terminated state
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) {
// go to main screen
// Navigator.of(context).pushNamed(route);
}
});
var lock = Lock();
///stream listener
///only work in the foreground
FirebaseMessaging.onMessage.listen((message) async {
if (!lock.locked) {
await lock.synchronized(() {
String preMsgId = context.read<TicketsQueueProvider>().preMsgId;
String newMsgId = message.messageId;
if (newMsgId != preMsgId) {
LocalNotificationService.display(message);
}
Provider.of<TicketsQueueProvider>(context, listen: false)
.setPreMsgId(newMsgId);
});
}
});
///stream listener
///only when app in the background but not close, and user tap notifiction
///user tap notification
FirebaseMessaging.onMessageOpenedApp.listen((message) {
// do nothing, could go to previous screen(route)
});
}
}
- The reason I added the
synchronizedlock is that during real-device testing on iOS I noticed that FirebaseMessaging.onMessage would receive the same message.id more than once. So in the project I useLockto ensure only one message.id is compared at a time, and use Provider to globally store the previous message.id.
Add a new file under lib/, local_notification_service.dart
class LocalNotificationService {
///Singleton pattern
///Goal: ensure that a class produces only one instance and provides a unified way to access it
static final LocalNotificationService _notificationService =
LocalNotificationService._internal();
factory LocalNotificationService() {
return _notificationService;
}
LocalNotificationService._internal();
static void requestIOSPermissions() {
notificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
static final FlutterLocalNotificationsPlugin notificationsPlugin =
FlutterLocalNotificationsPlugin();
static void initialize(
BuildContext context, String defaultRouteForMessage) async {
final InitializationSettings initializationSettings = InitializationSettings(
///android default icon
///route should be: {project}\android\app\src\main\res\drawable\YOUR_APP_ICON.png
android: AndroidInitializationSettings("@drawable/ic_notification"),
///ios default setting
iOS: IOSInitializationSettings(
requestSoundPermission: false,
requestBadgePermission: false,
requestAlertPermission: false,
));
/// Create an Android Notification Channel.
/// We use this channel in the `AndroidManifest.xml` file to override the
/// default FCM channel to enable heads up notifications.
AndroidNotificationChannel channel = const AndroidNotificationChannel(
'high_importance_channel', // id
'High Importance Notifications', // title
importance: Importance.high,
);
await notificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
await notificationsPlugin.initialize(initializationSettings,
onSelectNotification: (String route) async {
print('---selected notification');
// Navigator.of(context)
// .pushNamedAndRemoveUntil(defaultRouteForMessage, (route) => false);
// Navigator.of(context).pushNamed(defaultRouteForMessage);
});
}
static void testDisplay() async {
try {
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
/// notification settings such as channel, priority, etc.
final notificationDetails = NotificationDetails(
android: AndroidNotificationDetails(
'high_importance_channel', //channel id
'High Importance Notifications', //channel name
importance: Importance.max,
priority: Priority.high,
),
);
await notificationsPlugin.show(
id,
'notificaion title',
'notificaion body',
notificationDetails,
payload: "",
);
} catch (e) {
print(e);
}
}
static void display(RemoteMessage message) async {
try {
/// ensure each id is unique by using datetime
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
AndroidNotificationDetails _androidNotificationDetails =
AndroidNotificationDetails(
'high_importance_channel', 'High Importance Notifications',
playSound: true,
priority: Priority.high,
importance: Importance.max,);
IOSNotificationDetails _iosNotificationDetails = IOSNotificationDetails(
// presentAlert: bool?, // Present an alert when the notification is displayed and the application is in the foreground (only from iOS 10 onwards)
// presentBadge: bool?, // Present the badge number when the notification is displayed and the application is in the foreground (only from iOS 10 onwards)
// presentSound: bool?, // Play a sound when the notification is displayed and the application is in the foreground (only from iOS 10 onwards)
// sound: String?, // Specifics the file path to play (only from iOS 10 onwards)
// badgeNumber: int?, // The application's icon badge number
// attachments: List<IOSNotificationAttachment>?, (only from iOS 10 onwards)
// subtitle: String?, //Secondary description (only from iOS 10 onwards)
// threadIdentifier: String? (only from iOS 10 onwards)
);
NotificationDetails platformChannelSpecifics = NotificationDetails(
android: _androidNotificationDetails, iOS: _iosNotificationDetails);
await notificationsPlugin.show(
id,
message.notification.title,
message.notification.body,
// notificationDetails,
platformChannelSpecifics,
payload: ""
/// pass the route in
// payload: message.data["route"],
);
} catch (e) {
print(e);
}
}
}
Change Log
- 20220328 - init
- 20260501–translate by claude code