Muhammad Uzair
Firebase

Flutter Push Notifications with Firebase: Full Guide

Published: 2026-10-07 · 10 min read

Everything about FCM in Flutter: Android and iOS setup, permissions, token management, foreground/background/terminated handling, and the pitfalls checklist.

Push notifications are table stakes for any serious mobile app — and Firebase Cloud Messaging (FCM) remains the default choice for Flutter. But between Android 13+ permission changes, iOS APNs quirks, and the three app states (foreground, background, terminated), getting them right takes more than the 5-minute quickstart suggests. This is the complete guide I wish I had.


1. Setup: Packages and Firebase Project

Add the packages:

dependencies:
  firebase_core: ^3.8.0
  firebase_messaging: ^15.2.0

Initialize Firebase before runApp, and register the background handler before anything else:

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

// MUST be a top-level function — not a class method
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  debugPrint('Background message: ${message.notification?.title}');
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(const MyApp());
}

The @pragma('vm:entry-point') annotation is critical — without it, tree-shaking removes your handler in release builds and background messages silently stop working. This is the #1 "works in debug, broken in production" cause I see in audits.


2. Android Configuration

  1. Add google-services.json to android/app/.
  2. Apply the plugin in android/app/build.gradle.kts:
plugins {
    id("com.android.application")
    id("com.google.gms.google-services")
}
  1. For Android 13+, notification permission is runtime — request it explicitly (see step 4).

3. iOS Configuration (The Fiddly Part)

  1. In the Firebase console: Project Settings → Cloud Messaging → APNs Authentication Key — upload your .p8 key from Apple Developer. (Use a key, not certificates — keys don't expire yearly.)
  2. In Xcode: enable Push Notifications and Background Modes → Remote notifications capabilities.
  3. That's it for FCM — no AppDelegate changes needed with recent firebase_messaging versions.

iOS Simulator cannot receive push notifications. You must test on a physical device. This wastes everyone's first afternoon — now it won't waste yours.


4. Requesting Permission

iOS always needs explicit permission. Android needs it only on 13+:

final messaging = FirebaseMessaging.instance;

final settings = await messaging.requestPermission(
  alert: true,
  badge: true,
  sound: true,
);

if (settings.authorizationStatus == AuthorizationStatus.authorized) {
  debugPrint('Notifications allowed');
} else {
  debugPrint('Notifications denied — guide the user to settings');
}

Pro tip: don't ask on first launch. Ask after the user experiences value — e.g., after their first order ships or first message arrives. Apps that ask at the right moment see 2-3x higher opt-in rates.


5. Getting and Managing the FCM Token

The token identifies the device. Get it, and listen for refreshes:

final token = await messaging.getToken();
debugPrint('FCM token: $token');

// Tokens refresh — always listen and update your backend
messaging.onTokenRefresh.listen((newToken) {
  // POST newToken to your backend, mapped to the user ID
  userRepository.updatePushToken(newToken);
});

Send the token to your backend mapped to the user account, not just the device — otherwise logged-out devices keep receiving someone else's notifications.


6. Handling All Three App States

This is where most implementations fall apart. You need all three:

// A. FOREGROUND — app open on screen
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  // FCM does NOT show a notification automatically here.
  // Show your own UI: snackbar, banner, or local notification.
  _showInAppBanner(message.notification?.title);
});

// B. BACKGROUND — app minimized
// Handled by _firebaseMessagingBackgroundHandler (registered in main).
// System tray shows the notification automatically.

// C. TERMINATED — app killed
final initialMessage = await messaging.getInitialMessage();
if (initialMessage != null) {
  // App was launched FROM a notification tap — deep-link accordingly
  _handleNotificationTap(initialMessage);
}

// Taps on notifications (background state)
FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationTap);

For heads-up notifications while in the foreground, pair FCM with flutter_local_notifications and create an Android notification channel:

const channel = AndroidNotificationChannel(
  'high_importance_channel',
  'High Importance Notifications',
  importance: Importance.high,
);
await flutterLocalNotificationsPlugin
    .resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin>()
    ?.createNotificationChannel(channel);

7. Testing Without Losing Your Mind

  • Firebase Console → Cloud Messaging → Send test message: paste the FCM token, send. Fastest loop for manual testing.
  • Test all three states every time: foreground, background, killed. They behave differently and each has broken in production for someone.
  • Test on low-end Android devices, not just your flagship — OEM battery optimizers (Xiaomi, Huawei, Oppo) aggressively kill background FCM. Document this for your users with a "disable battery optimization" help screen if notifications are core to your app.

8. Common Pitfalls Checklist

  • Background handler missing @pragma('vm:entry-point') → silent failure in release
  • Not handling token refresh → notifications stop after token rotation
  • iOS: testing on simulator → nothing will ever arrive
  • Data-only messages on iOS → need content-available: true and won't wake a force-quit app
  • Sending to token instead of user → wrong person gets notified after logout
  • No deep-link handling on tap → notification opens the app to the wrong screen

Push notifications done right feel like magic; done wrong they're the #1 source of 1-star reviews. Follow the three-state pattern above, test on real devices, and respect the user's attention — every notification should earn its interruption.


Need FCM, deep linking, or a full notification system built into your Flutter app? That's exactly what I do — get in touch.

Frequently Asked Questions

How do I handle push notifications in all three app states in Flutter?

You need all three: FirebaseMessaging.onMessage for foreground (FCM does not show a notification automatically here, so show your own snackbar or banner), the top-level background handler registered in main() for background, and getInitialMessage plus onMessageOpenedApp for the terminated state and taps.

Why do my Flutter push notifications work in debug but not in release?

The background handler is almost certainly missing the @pragma('vm:entry-point') annotation - without it, tree-shaking removes the handler in release builds and background messages silently stop working. This is the number-one works-in-debug, broken-in-production cause.

When should I ask for notification permission in my Flutter app?

Do not ask on first launch. Ask after the user experiences value - for example after their first order ships or first message arrives - because apps that ask at the right moment see 2-3x higher opt-in rates. iOS always needs explicit permission; Android needs it only on 13+.

Why are my FCM push notifications not arriving on iOS?

You must test on a physical device - the iOS Simulator cannot receive push notifications. Also upload an APNs Authentication Key (.p8) in the Firebase console (keys do not expire yearly like certificates) and enable the Push Notifications and Background Modes capabilities in Xcode.

Related Articles

← Back to all articles