The Flutter Kit logoThe Flutter Kit
Guide

How to Add Push Notifications to a Flutter App with Firebase Cloud Messaging

Wire Firebase Cloud Messaging into a Flutter app from zero: add firebase_messaging, request iOS and Android permissions, capture the device token, and handle foreground, background, and terminated messages. Do it by hand below, or flip it on in The Flutter Kit with FCM diagnostics and copyable tokens.

Last updated: 2026-06-19 8 min read By Ahmed Gagan, Flutter Engineer
Quick Answer

To add push notifications to a Flutter app, you install firebase_messaging, request notification permission on iOS and Android 13+, capture the device's FCM registration token, and register three handlers for foreground, background, and terminated-state messages. FCM is the free transport that reaches both stores from one Dart codebase. The Flutter Kit ($69 one-time, unlimited projects, lifetime updates, full source ownership) ships this pre-wired with local + FCM push diagnostics and copyable tokens, so you test a real notification on a device in minutes instead of fighting APNs entitlements.

Transport
Firebase Cloud Messaging over APNs (iOS) + native FCM (Android)
Three states to handle
Foreground (onMessage), background, terminated (getInitialMessage)
Pre-wired option
The Flutter Kit — FCM diagnostics + copyable tokens, $69 one-time

Why the message lifecycle is where DIY push breaks

The getToken-to-onMessage snippet looks like a ten-minute job, and that is the trap. The real cost of adding push notifications to a Flutter app lives in the three app states, because each is a different code path that fails independently. Foreground messages never draw a system banner on their own, so you must render one yourself or they appear to vanish. Background and terminated messages execute in a separate Dart isolate, which is why the handler has to be top-level, annotated with vm:entry-point, and re-initialize Firebase — get this wrong and data pushes silently no-op in release builds. On iOS, none of it works at all until an APNs auth key is uploaded and the Push Notifications plus Background Modes capabilities are enabled, a step that compiles fine and then delivers nothing. Add token rotation, Android notification channels, and permission prompts, and a 'quick' push feature becomes a day of device testing. The Flutter Kit's value here is not the listener code; it is the diagnostics screen that shows live permission status, a copyable token, and one-tap test sends, so you see exactly which state is broken instead of guessing.

  • Foreground messages need flutter_local_notifications or no banner shows
  • Background/terminated handlers run in a separate isolate and must be top-level
  • iOS delivers nothing until an APNs key and capabilities are configured
  • Tokens rotate — onTokenRefresh must upsert or sends silently fail

When you should wire FCM by hand instead

Honesty first: if all you need is a single local reminder notification with no server push, skip FCM entirely and use flutter_local_notifications on its own — pulling in Firebase Messaging and APNs setup is overkill for a scheduled alarm. The same is true if you've already standardized on a different provider such as OneSignal for its segmentation and dashboard, where layering raw FCM underneath only duplicates work. And if your app deliberately avoids Firebase, hand-rolling the messaging layer (or going straight to APNs and FCM HTTP v1 from your own backend) keeps your dependency surface clean. The Flutter Kit earns its $69 when you want push as one part of a real app — tied to Firebase Auth, Firestore-stored tokens, and go_router deep links — already assembled and verified on a device, with the diagnostics screen that turns 'why isn't this arriving' from a lost afternoon into a glance.

Wire Firebase Cloud Messaging into Flutter, step by step

This is the from-zero FCM path. Each step is a piece of plumbing The Flutter Kit already ships behind a diagnostics screen — do it by hand to understand the message lifecycle, then decide whether you want to maintain the APNs and channel setup yourself.

  1. 1

    Add firebase_messaging and connect FlutterFire

    Add firebase_messaging (plus flutter_local_notifications to render foreground banners) and run flutterfire configure so firebase_options.dart exists for every platform. On iOS you also need an APNs key uploaded to the Firebase console and the Push Notifications capability enabled in Xcode — FCM on iOS is a thin layer over APNs and silently does nothing without it.

    flutter pub add firebase_messaging flutter_local_notifications
    dart pub global activate flutterfire_cli
    flutterfire configure
  2. 2

    Request notification permission the platform way

    iOS and Android 13+ both gate notifications behind a runtime permission prompt. Call requestPermission() and check the authorizationStatus before assuming delivery works — a denied prompt is the number-one reason 'my pushes don't arrive'. The kit surfaces the live permission status on its diagnostics screen so you never guess.

    final settings = await FirebaseMessaging.instance.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );
    if (settings.authorizationStatus == AuthorizationStatus.denied) {
      // route the user to system settings
    }
  3. 3

    Get the FCM token and watch it refresh

    The registration token is the address you send a push to. Read it with getToken(), persist it to Firestore against the user, and also listen to onTokenRefresh — tokens rotate on reinstall, restore, and data clears, and a stale token means silent delivery failures. The kit makes the current token copyable so you can paste it straight into a test send.

    final token = await FirebaseMessaging.instance.getToken();
    FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
      // upsert newToken to the user's doc in Firestore
    });
  4. 4

    Handle foreground messages explicitly

    When the app is in the foreground, FCM does NOT draw a system notification for you — onMessage fires with the payload and you decide what to show. Most apps render it via flutter_local_notifications so a banner still appears. Forget this and your pushes look broken only while the app is open, which is maddening to debug.

    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      final n = message.notification;
      if (n != null) {
        localNotifications.show(
          n.hashCode, n.title, n.body, channelDetails);
      }
    });
  5. 5

    Register a top-level background handler

    Background and terminated messages run in a separate isolate, so the handler MUST be a top-level or static function annotated with @pragma('vm:entry-point') and registered before runApp. You also call Firebase.initializeApp() inside it because the isolate has no app instance. This isolate gotcha is the single most common FCM mistake in DIY Flutter builds.

    @pragma('vm:entry-point')
    Future<void> _bgHandler(RemoteMessage message) async {
      await Firebase.initializeApp();
      // process data payload, e.g. update a local DB
    }
    
    void main() {
      FirebaseMessaging.onBackgroundMessage(_bgHandler);
      runApp(const App());
    }
  6. 6

    Handle taps that open the app (background + terminated)

    A user tapping a notification is a navigation event. Use onMessageOpenedApp for taps when the app was backgrounded, and getInitialMessage() for a cold start from terminated — the latter returns the message that launched the app so you can deep-link to the right screen. In the kit this routes through go_router so a tapped push lands on the correct route.

    FirebaseMessaging.onMessageOpenedApp.listen((m) => _goTo(m.data['route']));
    final initial = await FirebaseMessaging.instance.getInitialMessage();
    if (initial != null) _goTo(initial.data['route']);
  7. 7

    Send a real test and verify it lands

    Confirm the full loop before writing any backend. Copy the device token and send a test from the Firebase console (Cloud Messaging > test message) or curl the FCM v1 API. Verify foreground, background, and terminated states separately — they exercise different code paths. The Flutter Kit's diagnostics screen does this with one tap so you prove delivery on a real device immediately.

    curl -X POST https://fcm.googleapis.com/v1/projects/PROJECT_ID/messages:send \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"message":{"token":"DEVICE_FCM_TOKEN","notification":{"title":"Hi","body":"It works"}}}'

Frequently Asked Questions

Do I still need firebase_messaging if I use The Flutter Kit's push notifications?
The Flutter Kit is built on firebase_messaging under the hood — it doesn't replace FCM, it pre-wires it. You don't install or configure the package yourself; the kit ships the permission flow, token capture with onTokenRefresh, the three message handlers, and a diagnostics screen with copyable tokens. You add your APNs key and Firebase project, and push works on iOS, Android, and Web from day one.
Why do push notifications arrive on Android but not iOS in my Flutter app?
Almost always because iOS FCM is layered on APNs and the APNs piece isn't configured. You need an APNs authentication key (.p8) uploaded to the Firebase console, the Push Notifications capability and Background Modes enabled in Xcode, and a real device — the iOS simulator can't receive remote pushes. The Flutter Kit documents each of these in its setup checklist and shows live permission status on its diagnostics screen so you spot the gap immediately.
How do I handle a terminated-state push notification tap in Flutter?
When the app is fully terminated, a notification tap that launches it won't fire onMessageOpenedApp — instead you read FirebaseMessaging.instance.getInitialMessage() once at startup, which returns the RemoteMessage that opened the app (or null). Use its data payload to deep-link to the right screen. The Flutter Kit routes this through go_router so a cold-start tap lands on the correct route automatically.
Why doesn't a banner show for foreground push notifications in my Flutter app?
By design. When the app is in the foreground, FCM hands you the payload via onMessage but does not draw a system notification — you decide whether to show one. The standard fix is to render it with flutter_local_notifications (and declare an Android notification channel). The Flutter Kit wires foreground messages into a local notification automatically so behavior is consistent across all three app states.
Why must the FCM background handler be a top-level function in Flutter?
Background and terminated messages are delivered in a separate background isolate that has no access to your app's existing state, so the handler can't be a closure or instance method — it must be a top-level or static function annotated with @pragma('vm:entry-point') and registered with onBackgroundMessage before runApp. You also call Firebase.initializeApp() inside it. This isolate requirement is the most common reason DIY background pushes work in debug but silently fail in release; the kit's handler is set up correctly out of the box.
Can I test push notifications in The Flutter Kit without building a backend first?
Yes. The kit ships a push diagnostics screen that surfaces the live permission status and a copyable FCM token, so you can paste the token into the Firebase console's test-message tool (or a curl to the FCM v1 API) and confirm delivery on a real device in minutes — before you write a single line of server code. You verify foreground, background, and terminated delivery separately, then build your sending backend once you know the client works.

Keep exploring

Skip the APNs and isolate gotchas

The Flutter Kit ships Firebase Cloud Messaging pre-wired — permissions, token capture, foreground/background/terminated handlers, go_router deep links, and a diagnostics screen with copyable tokens. $69 one-time, unlimited projects, lifetime updates, full source ownership. Prove a real push on a device in minutes.

Get The Flutter Kit — $69

One-time purchase · Lifetime updates · Unlimited projects