Flutter Push Notifications with Firebase Cloud Messaging (FCM): A Complete Setup Guide

Step-by-step guide to set up FCM push notifications in Flutter for Android and iOS, with code, permissions, background handling, and testing tips.

ASOasis
8 min read
Flutter Push Notifications with Firebase Cloud Messaging (FCM): A Complete Setup Guide

Image used for representation purposes only.

Overview

Flutter makes building cross‑platform apps fast, and Firebase Cloud Messaging (FCM) makes push notifications reliable. This guide walks you end‑to‑end through configuring FCM for a Flutter app on Android and iOS, covering permissions, background handlers, foreground presentation, notification channels, navigation on tap, topics, and testing from the Firebase Console and your own server.

What you’ll build:

  • A Flutter app initialized with Firebase
  • End‑to‑end push notification handling (foreground, background, terminated)
  • iOS APNs + Android channels and runtime permissions
  • Token management and topic subscriptions
  • Server-side examples using curl and Node.js (firebase-admin)

Prerequisites

  • Flutter SDK and a recent stable channel
  • A Firebase project (owner or editor permissions)
  • Apple Developer Program membership for iOS push (APNs)
  • Xcode (for iOS), Android Studio + Android SDKs (for Android)
  • A physical iOS device for APNs testing (the iOS simulator does not receive push). Android emulators with Google Play services can receive FCM.

1) Create your Firebase project and register apps

  1. In the Firebase console, create (or open) a project.
  2. Add iOS and Android apps:
    • iOS: Use your app’s exact bundle identifier (e.g., com.example.myapp).
    • Android: Use your app’s package name (e.g., com.example.myapp).
  3. Do not manually download and place config files if you plan to use FlutterFire CLI; it will handle that for you.

2) Configure Flutter with FlutterFire CLI

Install and run the CLI to link your Flutter app to Firebase and generate platform config.

dart pub global activate flutterfire_cli
flutterfire configure

What it does:

  • Links your Firebase project
  • Registers the platforms you choose
  • Generates lib/firebase_options.dart with DefaultFirebaseOptions for each platform

3) Add dependencies

Add Firebase Core and Messaging (and, optionally, local notifications for foreground presentation on Android).

# pubspec.yaml
dependencies:
  flutter: any
  firebase_core: any
  firebase_messaging: any
  flutter_local_notifications: any   # optional but recommended for Android foreground

Run:

flutter pub get

4) Initialize Firebase early

Initialize Firebase before using messaging and register the background handler.

// lib/main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'firebase_options.dart';
import 'package:flutter/material.dart';

@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // Background handler runs in its own isolate.
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  // TODO: handle background data (e.g., update local DB).
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  // iOS: control how notifications are displayed when app is in foreground
  await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
    alert: true,
    badge: true,
    sound: true,
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: Scaffold(body: Center(child: Text('FCM Setup'))));
  }
}

5) iOS setup (APNs and capabilities)

To receive push on iOS you must configure APNs and enable capabilities.

  • In Xcode, open ios/Runner.xcworkspace.
  • Select Runner target → Signing & Capabilities → add:
    • Push Notifications
    • Background Modes → check Remote notifications
  • In the Firebase console: Project Settings → Cloud Messaging → Apple app configuration.
    • Upload your APNs Auth Key (.p8) and enter Key ID and Team ID, or use certificates. Ensure the iOS bundle ID matches.
  • Build to a physical device. The iOS simulator will not receive remote push.

Tip: For silent background updates, your payload must include "content-available": 1 (APNs), and you must avoid showing an alert/sound for that specific message.

6) Android setup (permissions and manifest)

  • Android 13+ requires the POST_NOTIFICATIONS runtime permission.
  • Android 8.0+ requires a NotificationChannel for high-importance notifications.

Add the permission to android/app/src/main/AndroidManifest.xml:

<manifest ...>
  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
  <application
      android:name="io.flutter.app.FlutterApplication"
      android:icon="@mipmap/ic_launcher"
      android:label="@string/app_name">

    <!-- Optional default icon/color for notifications -->
    <meta-data
      android:name="com.google.firebase.messaging.default_notification_icon"
      android:resource="@drawable/ic_stat_notification" />
    <meta-data
      android:name="com.google.firebase.messaging.default_notification_color"
      android:resource="@color/notification_color" />
  </application>
</manifest>

Note: If you use a custom Application class, ensure Firebase is properly initialized. Most Flutter apps don’t need to change the application name.

7) Request permission and get the FCM token

On iOS (and Android 13+), you must request notification permission at runtime. Then retrieve and monitor the device’s FCM registration token.

Future<void> initPush() async {
  final messaging = FirebaseMessaging.instance;

  // Request permission (iOS + Android 13+)
  final settings = await messaging.requestPermission(
    alert: true,
    announcement: false,
    badge: true,
    carPlay: false,
    criticalAlert: false,
    provisional: false, // set true for iOS provisional (no prompt)
    sound: true,
  );
  debugPrint('Permission status: ${settings.authorizationStatus}');

  // Get the token
  final token = await messaging.getToken();
  debugPrint('FCM token: $token');
  // TODO: send token to your server and associate with the signed-in user

  // Listen for token refresh
  FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
    // TODO: update token on your server
  });
}

Call initPush() after Firebase initialization and, ideally, after onboarding or login to store tokens server-side.

8) Handle messages: foreground, background, terminated

  • Foreground: FirebaseMessaging.onMessage fires. Show a local notification if you want a banner on Android.
  • App opened from background: FirebaseMessaging.onMessageOpenedApp fires on tap.
  • App opened from a terminated state: use getInitialMessage() to read the notification that launched the app.
void registerMessageHandlers() {
  FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    debugPrint('Foreground message: ${message.messageId}');
    // Optionally display a local notification on Android; iOS presentation is controlled above.
  });

  FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
    _handleNotificationNavigation(message);
  });
}

Future<void> checkInitialMessage() async {
  final initial = await FirebaseMessaging.instance.getInitialMessage();
  if (initial != null) {
    _handleNotificationNavigation(initial);
  }
}

void _handleNotificationNavigation(RemoteMessage message) {
  final target = message.data['screen'];
  // Use your navigator to route, e.g., Navigator.pushNamed(context, target)
}

Call registerMessageHandlers() and checkInitialMessage() in your app startup after Firebase initialization.

9) Foreground banners on Android with flutter_local_notifications

By default, Android does not show a system banner when your app is in the foreground. Use flutter_local_notifications to surface a notification.

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin fln = FlutterLocalNotificationsPlugin();

const AndroidNotificationChannel highImportanceChannel = AndroidNotificationChannel(
  'high_importance', 'High Importance',
  description: 'Used for important notifications.',
  importance: Importance.max,
);

Future<void> setupLocalNotifications() async {
  const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
  const iosInit = DarwinInitializationSettings();
  await fln.initialize(const InitializationSettings(android: androidInit, iOS: iosInit));

  // Create the channel once (Android 8+)
  await fln.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
      ?.createNotificationChannel(highImportanceChannel);

  // Show a banner when a foreground FCM message arrives
  FirebaseMessaging.onMessage.listen((message) {
    final notification = message.notification;
    if (notification != null) {
      fln.show(
        notification.hashCode,
        notification.title,
        notification.body,
        NotificationDetails(
          android: AndroidNotificationDetails(
            highImportanceChannel.id,
            highImportanceChannel.name,
            channelDescription: highImportanceChannel.description,
            icon: '@mipmap/ic_launcher',
            importance: Importance.max,
            priority: Priority.high,
          ),
          iOS: const DarwinNotificationDetails(),
        ),
        payload: message.data['screen'],
      );
    }
  });
}

10) Data vs notification payloads

  • Notification payload: { notification: { title, body }, data: {...} }
    • The system can display it automatically (background), especially on Android.
  • Data-only payload: { data: {...} }
    • Gives you full control. On iOS, include APNs content-available: 1 for background delivery and enable Background Modes. Avoid heavy work; iOS limits execution time.

Choose the model that best fits your UX. Many apps send both: show an alert and include routing info in data.

11) Send a test message from Firebase Console

  • Cloud Messaging → Send your first message.
  • Paste a device token or select a target app.
  • Include a simple title/body and optional custom data (e.g., screen: inbox).

Great for initial validation.

12) Send messages from your server

HTTP v1 via curl

Obtain an OAuth2 access token from a service account, then call FCM.

# Using gcloud (with GOOGLE_APPLICATION_CREDENTIALS set to your service account JSON)
ACCESS_TOKEN=$(gcloud auth application-default print-access-token)
PROJECT_ID=your-project-id

curl -X POST \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json; UTF-8" \
  https://fcm.googleapis.com/v1/projects/$PROJECT_ID/messages:send \
  -d '{
    "message": {
      "token": "DEVICE_FCM_TOKEN",
      "notification": {"title": "Hello", "body": "World"},
      "data": {"screen": "inbox", "orderId": "123"},
      "android": {"notification": {"channel_id": "high_importance"}},
      "apns": {"payload": {"aps": {"content-available": 1}}}
    }
  }'

Node.js with firebase-admin

import admin from 'firebase-admin';
import serviceAccount from './service-account.json' assert { type: 'json' };

admin.initializeApp({ credential: admin.credential.cert(serviceAccount) });

const res = await admin.messaging().send({
  token: 'DEVICE_FCM_TOKEN',
  notification: { title: 'Hi', body: 'Welcome!' },
  data: { screen: 'home' },
  android: { notification: { channelId: 'high_importance' } },
  apns: { payload: { aps: { contentAvailable: true } } },
});
console.log('Sent:', res);

13) Navigate on notification tap

Use onMessageOpenedApp for warm starts and getInitialMessage() for cold starts. Include routing data in your payload.

Payload example (data fields):

{
  "message": {
    "token": "...",
    "notification": { "title": "New message", "body": "Open your inbox" },
    "data": { "screen": "inbox", "threadId": "abc" }
  }
}

14) Topics and user segmentation

  • Subscribe clients to topics:
await FirebaseMessaging.instance.subscribeToTopic('news');
await FirebaseMessaging.instance.unsubscribeFromTopic('promotions');
  • Send to a topic from your server by specifying topic: 'news' instead of token.
  • Combine with custom claims or server-side audience rules for precise targeting.

15) Web support (brief)

For Flutter Web, you need:

  • A public VAPID key set in Firebase console
  • A firebase-messaging-sw.js service worker at the site root
  • getToken(vapidKey: '...') in your web build

Because service worker scope and hosting paths vary, consult the FlutterFire web messaging docs when adding web.

16) Troubleshooting checklist

  • iOS device not receiving push:
    • Verify APNs Auth Key is uploaded and Bundle ID matches.
    • Ensure Push Notifications + Background Modes (Remote notifications) are enabled.
    • Test on a physical device; simulator won’t receive push.
  • Android foreground shows nothing:
    • Use flutter_local_notifications in onMessage to display a local notification.
    • Ensure a NotificationChannel exists with high importance.
  • Permission prompts not appearing:
    • iOS: calling requestPermission more than once after denial won’t show a dialog; direct users to Settings.
    • Android 13+: ensure you requested POST_NOTIFICATIONS at runtime (firebase_messaging’s requestPermission).
  • Token is null or changes often:
    • Ensure Google Play services are up to date (Android) and APNs is configured (iOS).
    • Handle onTokenRefresh and update your backend.
  • Data-only silent pushes on iOS not delivered:
    • Include content-available: 1, remove alert/sound, and respect Apple’s execution limits. Delivery is not guaranteed when the device is power constrained.

17) Security and delivery best practices

  • Never ship service account keys in the client app. Send pushes from your backend only.
  • Associate FCM tokens with authenticated user IDs and device metadata on your server.
  • Use topics or condition expressions to avoid storing per-user token lists where possible.
  • Keep payloads small; large data reduces reliability and can be delayed on constrained networks.
  • For sensitive actions, require in-app confirmation instead of executing directly from push.

18) Summary

You configured Firebase with FlutterFire, requested permissions, obtained and refreshed tokens, handled foreground/background/terminated deliveries, added Android channels and iOS capabilities, and learned how to send pushes from the console and your own server. With this foundation, you can implement rich, reliable messaging and deep navigation across Android and iOS.

Related Posts