Step-by-Step Guide: Setting Up Firebase Cloud Messaging (FCM) in Flutter
This guide will walk you through setting up Firebase Cloud Messaging (FCM) in your Flutter application, covering both iOS and Android configurations and how to handle incoming messages.
Overview :
iOS Setup
- Enabling capabilities in Xcode.
- Uploading APNs authentication key to Firebase.
- Ensuring method swizzling is enabled.
Android Setup
- Checking Google Play Services availability.
- Implementing checks in the main activity file.
Receiving Messages
- Handling foreground and background messages.
- Managing user interaction with notifications.
Sending a Test Message
- Installing the FCM plugin.
- Accessing the registration token.
- Sending a test notification message.
Setting Up FCM for iOS Enable App Capabilities in Xcode
Open Your Project in Xcode
- Navigate to your Flutter project directory.
- Open the
ios
folder. - Open
Runner.xcworkspace
in Xcode.
Enable Push Notifications
- Select your project (Runner) in Xcode.
- Go to the Signing & Capabilities tab.
- Click
+ Capability
, search for Push Notifications, and select it.
Enable Background Modes
- In the same tab, click
+ Capability
again. - Search for Background Modes and select it.
- Check both Background fetch and Remote notifications.
Upload Your APNs Authentication Key
Create an APNs Certificate
- Go to the Apple Developer Member Center and log in.
- Under Certificates, click
+
to create a new certificate. - Choose Apple Push Notification service (APNs) and follow the instructions to generate and download your
.cer
file.
Convert the Certificate to a .p12 File
- Open the
.cer
file in Keychain Access. - Find the certificate, right-click it, and select Export.
- Save it as a
.p12
file and set a password if prompted.
Upload the Certificate to Firebase
- Open your project in the Firebase console.
- Click the gear icon ⚙️ next to Project Overview and select Project Settings.
- Navigate to the Cloud Messaging tab and click the Upload Certificate button.
- Select your
.p12
file and enter the password. - Ensure that the Bundle ID matches your app’s bundle ID in Xcode, then click Save.
Method Swizzling :
Ensure that method swizzling is enabled by checking ios/Runner/Info.plist
for the key FirebaseAppDelegateProxyEnabled
set to YES.
Setting Up FCM for Android
Google Play Services Requirement
Ensure your Flutter app targets Android 4.4 (KitKat) or higher. This is set in android/app/build.gradle
:
android {
compileSdkVersion 33 // Or higher
defaultConfig {
applicationId "com.your_package_name"
minSdkVersion 19 // Or higher
targetSdkVersion 33 // Or higher
versionCode 1
versionName "1.0"
}
}
Check Google Play Services Availability
Open Main Activity File
- Navigate to
android/app/src/main/java/com/your_package_name/MainActivity.java
orMainActivity.kt
.
Add Google Play Services Check
For Java:
import android.os.Bundle;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
checkGooglePlayServices();
}
private void checkGooglePlayServices() {
GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
int resultCode = apiAvailability.isGooglePlayServicesAvailable(this);
if (resultCode != ConnectionResult.SUCCESS) {
// Handle error: Google Play Services not available
}
}
@Override
protected void onResume() {
super.onResume();
checkGooglePlayServices();
}
}
For Kotlin:
import android.os.Bundle
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkGooglePlayServices()
}
private fun checkGooglePlayServices() {
val apiAvailability = GoogleApiAvailability.getInstance()
val resultCode = apiAvailability.isGooglePlayServicesAvailable(this)
if (resultCode != ConnectionResult.SUCCESS) {
// Handle error: Google Play Services not available
}
}
override fun onResume() {
super.onResume()
checkGooglePlayServices()
}
}
Allow Users to Install Google Play Services
Add this line in the checkGooglePlayServices
method to prompt users to install:
apiAvailability.makeGooglePlayServicesAvailable(this);
Or in Kotlin:
apiAvailability.makeGooglePlayServicesAvailable(this);
Receiving Messages in Your App
Install the FCM Plugin
From your project root, run:
flutter pub add firebase_messaging
Access the Registration Token
Retrieve the device’s registration token in your lib/main.dart
:
import 'package:firebase_messaging/firebase_messaging.dart';
Future<void> getToken() async {
String? token = await FirebaseMessaging.instance.getToken();
print('FCM Token: $token');
}
Handle Incoming Messages
Foreground Messages
Listen to messages when your app is in the foreground:
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Received message in foreground: ${message.data}');
if (message.notification != null) {
print('Notification: ${message.notification}');
}
});
Background Messages
Register a background message handler in your lib/main.dart
. This function must be top-level:
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print("Handling a background message: ${message.messageId}");
}
void main() {
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(MyApp());
}
Handling User Interaction with Notifications
Add the following code to handle user interactions when a notification is tapped:
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
setupInteractedMessage();
}
Future<void> setupInteractedMessage() async {
RemoteMessage? initialMessage =
await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
_handleMessage(initialMessage);
}
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
}
void _handleMessage(RemoteMessage message) {
if (message.data['type'] == 'chat') {
Navigator.pushNamed(context, '/chat', arguments: ChatArguments(message));
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("FCM Example")),
body: Center(child: Text("...")),
),
);
}
}
To further optimize and separate the code while introducing additional functionality, we’ll follow these principles:
- Separation of Concerns: Each responsibility (Firebase messaging, token management, notification handling, etc.) will be modular.
- Efficiency: The code will minimize redundant operations, optimize background handling, and prepare for future extensions.
- Additional Functions: Add error handling, message parsing, logging, and customizable actions for notifications.
Updated Project Structure:
lib/
main.dart
services/
firebase_service.dart
handlers/
notification_handler.dart
utils/
message_parser.dart
logger.dart
widgets/
my_app.dart
chat_page.dart
notification_page.dart
1. main.dart
(App Entry Point)
import 'package:flutter/material.dart';
import 'services/firebase_service.dart';
import 'widgets/my_app.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FirebaseService.initialize();
runApp(MyApp());
}
2. services/firebase_service.dart
(Firebase Initialization, Token Management, Background Handling)
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import '../utils/logger.dart';
class FirebaseService {
static Future<void> initialize() async {
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(_backgroundMessageHandler);
await _getToken();
}
static Future<void> _getToken() async {
try {
String? token = await FirebaseMessaging.instance.getToken();
if (token != null) {
Logger.log('FCM Token retrieved: $token');
// Add code to send the token to your backend server if needed
}
} catch (e) {
Logger.log('Error retrieving FCM Token: $e', isError: true);
}
}
@pragma('vm:entry-point')
static Future<void> _backgroundMessageHandler(RemoteMessage message) async {
Logger.log("Handling background message: ${message.messageId}");
// Handle background message here, e.g., save to local database
}
}
3. handlers/notification_handler.dart
(Notification Handling)
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import '../utils/message_parser.dart';
import '../utils/logger.dart';
class NotificationHandler {
static void initialize(BuildContext context) {
FirebaseMessaging.onMessage.listen((message) => _onMessage(message));
FirebaseMessaging.onMessageOpenedApp.listen((message) => _onOpenedMessage(message, context));
}
static Future<void> checkInitialMessage(BuildContext context) async {
RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
_onOpenedMessage(initialMessage, context);
}
}
static void _onMessage(RemoteMessage message) {
Logger.log('Foreground message received: ${message.data}');
var notificationData = MessageParser.parse(message);
if (notificationData != null) {
// Display in-app notifications or trigger relevant actions
Logger.log('Parsed Notification Data: $notificationData');
}
}
static void _onOpenedMessage(RemoteMessage message, BuildContext context) {
var notificationData = MessageParser.parse(message);
if (notificationData?.type == 'chat') {
Navigator.pushNamed(context, '/chat', arguments: notificationData);
} else if (notificationData?.type == 'notification') {
Navigator.pushNamed(context, '/notification', arguments: notificationData);
}
}
}
4. utils/message_parser.dart
(Message Parsing Utility)
This utility helps in extracting and organizing data from notifications so that it’s easier to handle different types of messages.
import 'package:firebase_messaging/firebase_messaging.dart';
class MessageParser {
static ParsedMessage? parse(RemoteMessage message) {
try {
String? type = message.data['type'];
String? content = message.data['content'];
String? sender = message.data['sender'];
if (type != null && content != null) {
return ParsedMessage(type: type, content: content, sender: sender);
}
} catch (e) {
// Handle parsing error
}
return null;
}
}
class ParsedMessage {
final String type;
final String content;
final String? sender;
ParsedMessage({required this.type, required this.content, this.sender});
}
5. utils/logger.dart
(Logging Utility)
The logging utility helps with clean and consistent logging across the app, especially for debugging and monitoring.
class Logger {
static void log(String message, {bool isError = false}) {
final logPrefix = isError ? '[ERROR]' : '[INFO]';
print('$logPrefix: $message');
}
}
6. widgets/my_app.dart
(Main App Widget)
import 'package:flutter/material.dart';
import '../handlers/notification_handler.dart';
import 'chat_page.dart';
import 'notification_page.dart';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
NotificationHandler.initialize(context);
NotificationHandler.checkInitialMessage(context);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'FCM Example',
home: Scaffold(
appBar: AppBar(title: Text("FCM Example")),
body: Center(child: Text("Firebase Messaging Example")),
),
routes: {
'/chat': (context) => ChatPage(),
'/notification': (context) => NotificationPage(),
},
);
}
}
7. widgets/chat_page.dart
(Chat Page)
import 'package:flutter/material.dart';
import '../utils/message_parser.dart';
class ChatPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ParsedMessage message = ModalRoute.of(context)!.settings.arguments as ParsedMessage;
return Scaffold(
appBar: AppBar(title: Text("Chat with ${message.sender}")),
body: Center(
child: Text("Chat content: ${message.content}"),
),
);
}
}
8. widgets/notification_page.dart
(Notification Page)
import 'package:flutter/material.dart';
import '../utils/message_parser.dart';
class NotificationPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ParsedMessage message = ModalRoute.of(context)!.settings.arguments as ParsedMessage;
return Scaffold(
appBar: AppBar(title: Text("Notification")),
body: Center(
child: Text("Notification content: ${message.content}"),
),
);
}
}
Efficiency and Added Functions:
Separation of Concerns: Firebase initialization, notification handling, logging, and message parsing are all separated.
Error Handling: Token retrieval and message parsing have error handling. This prevents the app from crashing in case of failures.
Additional Functionality:
Message Parsing: Now, notifications are parsed into ParsedMessage
objects, making it easier to handle various notification types.
Logging: A centralized logging utility is added for better tracking of messages, errors, and debugging.
Future-proof:
You can now easily extend the message parser for new notification types. You can add more Firebase services to FirebaseService
(e.g., analytics, crash reporting).
Sending a Test Message
Test Message from Firebase Console
- Install and run the app on your device. Make sure to accept the notification permission on iOS.
- In the Firebase console, navigate to the Cloud Messaging section.
- Click on Send your first message or Create Campaign.
- Fill in the title and message text, and click Next.
- In the Target section, select Add an FCM registration token and paste the registration token from your app.
- Click Test to send the notification.
Testing Notifications
When you send a notification while the app is in the background, it should appear on the device. Tapping the notification will open the app and trigger the appropriate handler.
Topic Messaging in Flutter with Firebase Cloud Messaging
To further enhance the Flutter application that integrates Firebase Cloud Messaging (FCM) topic messaging, we can introduce additional functionalities such as:
- Displaying a Settings Screen: Allow users to customize notification settings.
- Notification Channel Management: Manage notification channels for different types of messages.
- Logging Notifications: Store notifications in a local database for historical reference.
- Toggle Notification Sounds: Allow users to enable or disable notification sounds.
- Enhance Error Handling: Improve error handling across the app.
Updated Project Structure
/my_flutter_app
├── android
├── ios
├── lib
│ ├── main.dart
│ ├── firebase_options.dart
│ ├── services
│ │ └── messaging_service.dart
│ ├── screens
│ │ ├── home_screen.dart
│ │ └── settings_screen.dart
│ ├── models
│ │ ├── notification_model.dart
│ │ └── user_settings_model.dart
│ ├── widgets
│ │ ├── notification_widget.dart
│ │ ├── subscription_status_widget.dart
│ │ ├── custom_switch.dart
│ │ └── notification_list_widget.dart
└── pubspec.yaml
Step 1: Create User Settings Model
We’ll create a model to manage user notification settings.
user_settings_model.dart
// lib/models/user_settings_model.dart
class UserSettings {
bool isNotificationSoundEnabled;
UserSettings({this.isNotificationSoundEnabled = true});
Map<String, dynamic> toMap() {
return {
'isNotificationSoundEnabled': isNotificationSoundEnabled,
};
}
UserSettings.fromMap(Map<String, dynamic> map)
: isNotificationSoundEnabled = map['isNotificationSoundEnabled'] ?? true;
}
Step 2: Update Messaging Service
Enhance the messaging service to manage notification sounds and logging.
messaging_service.dart
// lib/services/messaging_service.dart
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/notification_model.dart';
import '../models/user_settings_model.dart';
class MessagingService {
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
final List<NotificationModel> _notifications = [];
UserSettings _userSettings = UserSettings();
Future<void> subscribeToTopic(String topic) async {
try {
await _messaging.subscribeToTopic(topic);
_saveSubscriptionStatus(topic, true);
print('Subscribed to topic: $topic');
} catch (e) {
print('Error subscribing to topic: $e');
}
}
Future<void> unsubscribeFromTopic(String topic) async {
try {
await _messaging.unsubscribeFromTopic(topic);
_saveSubscriptionStatus(topic, false);
print('Unsubscribed from topic: $topic');
} catch (e) {
print('Error unsubscribing from topic: $e');
}
}
Future<void> requestNotificationPermissions() async {
NotificationSettings settings = await _messaging.requestPermission();
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
print('User granted permission');
} else {
print('User declined or has not accepted permission');
}
}
void initializeMessageHandlers(Function(RemoteMessage) onMessageReceived) {
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Received a message in the foreground: ${message.notification?.title}');
onMessageReceived(message);
_addNotification(message);
if (_userSettings.isNotificationSoundEnabled) {
// Play notification sound (pseudo code)
// playNotificationSound();
}
});
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print('Message clicked! ${message.notification?.title}');
// Handle the click event
});
}
void loadUserSettings() async {
final prefs = await SharedPreferences.getInstance();
final settings = prefs.getString('userSettings');
if (settings != null) {
_userSettings = UserSettings.fromMap(settings);
}
}
Future<void> saveUserSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('userSettings', _userSettings.toMap().toString());
}
void _addNotification(RemoteMessage message) {
_notifications.add(NotificationModel(
title: message.notification?.title ?? 'No Title',
body: message.notification?.body ?? 'No Body',
));
}
List<NotificationModel> get notifications => _notifications;
void clearNotifications() {
_notifications.clear();
}
}
Step 3: Create Settings Screen
Create a settings screen where users can toggle notification sounds and other preferences.
settings_screen.dart
// lib/screens/settings_screen.dart
import 'package:flutter/material.dart';
import '../services/messaging_service.dart';
import '../widgets/custom_switch.dart';
class SettingsScreen extends StatefulWidget {
@override
_SettingsScreenState createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
final MessagingService _messagingService = MessagingService();
@override
void initState() {
super.initState();
_messagingService.loadUserSettings();
}
void _toggleNotificationSound(bool value) {
setState(() {
_messagingService._userSettings.isNotificationSoundEnabled = value;
_messagingService.saveUserSettings();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Settings')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CustomSwitch(
title: 'Notification Sound',
value: _messagingService._userSettings.isNotificationSoundEnabled,
onChanged: _toggleNotificationSound,
),
],
),
),
);
}
}
Step 4: Create a Custom Switch Widget
Create a custom switch widget to simplify the toggling of settings.
custom_switch.dart
// lib/widgets/custom_switch.dart
import 'package:flutter/material.dart';
class CustomSwitch extends StatelessWidget {
final String title;
final bool value;
final ValueChanged<bool> onChanged;
const CustomSwitch({
Key? key,
required this.title,
required this.value,
required this.onChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(title),
Switch(
value: value,
onChanged: onChanged,
),
],
);
}
}
Step 5: Enhance Notification Handling
We can enhance the notification handling to store notifications in a database (e.g., using SQLite). This part would involve creating a local database and inserting notifications as they are received. For this example, I will not add the full implementation, but I will give you a brief outline on how to implement SQLite in your Flutter app.
Additional Steps to Integrate SQLite
- Add Dependencies: Update
pubspec.yaml
to includesqflite
andpath_provider
.
dependencies:
sqflite: ^2.0.0+4
path_provider: ^2.0.2
2. Create Database Helper: Create a database helper class to manage database operations for notifications.
database_helper.dart
// lib/services/database_helper.dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import '../models/notification_model.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
factory DatabaseHelper() => _instance;
DatabaseHelper._internal();
static Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), 'notifications.db');
return await openDatabase(
path,
version: 1,
onCreate: (db, version) {
return db.execute(
'CREATE TABLE notifications(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT)',
);
},
);
}
Future<void> insertNotification(NotificationModel notification) async {
final db = await database;
await db.insert(
'notifications',
notification.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<List<NotificationModel>> getNotifications() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query('notifications');
return List.generate(maps.length, (i) {
return NotificationModel(
title: maps[i]['title'],
body: maps[i]['body'],
);
});
}
}
Integrate Database Helper in Messaging Service: Modify the MessagingService
to log notifications to the database when they are received.
messaging_service.dart Update
Future<void> _addNotification(RemoteMessage message) async {
NotificationModel newNotification = NotificationModel(
title: message.notification?.title ?? 'No Title',
body: message.notification?.body ?? 'No Body',
);
_notifications.add(newNotification);
await DatabaseHelper().insertNotification(newNotification); // Log to DB
}
With these enhancements, you have a more robust Flutter application with the following features:
- Users can toggle notification sounds.
- Settings are stored and retrieved using Shared Preferences.
- Notifications are logged in a local SQLite database for historical reference.
- Enhanced error handling and feedback mechanisms.
Conclusion
Incorporating Firebase Cloud Messaging (FCM) topic messaging in a Flutter app provides a powerful and scalable way to engage users by delivering timely and relevant information. By organizing messages around topics, developers can ensure that users only receive notifications on subjects they care about, improving user experience. This guide demonstrated how to effectively integrate and manage topic subscriptions in Flutter with optimized and efficient code. With FCM’s flexibility and ease of implementation, topic-based messaging is an excellent solution for apps requiring dynamic, high-throughput communication.
Let me know if you’d like any adjustments or additional details!