Skip to content
Blog

Firebase Messaging in Flutter: Push Notifications with Installation IDs

Firebase Cloud Messaging now registers Flutter apps via Installation IDs instead of tokens. Here's the current setup flow, per platform.

Published on • October 19, 2026

AI Assistant

Push notifications in Flutter still go through firebase_messaging, but the client flow described in Firebase’s official Flutter guide has shifted: registration is now built around Firebase Installation IDs (FIDs), with the old registration-token APIs marked deprecated. If you set FCM up two years ago, the setup steps below will look different in the parts that matter.

Install and platform prerequisites

Installation is one command plus a Firebase-initialized project:

flutter pub add firebase_messaging

The per-platform prerequisites are where teams lose time:

  • iOS: enable push notifications and the Background fetch and Remote notifications background modes in Xcode. Then upload an APNs authentication key (a .p8 file from the Apple Developer Member Center) in the Firebase console under Settings → Cloud Messaging — at least one key (dev, prod, or both) is required. The guide also notes that method swizzling is required for the FCM Flutter plugin on Apple devices; without it, FCM registration handling doesn’t function properly.
  • Android: devices need Android 7.0 or higher with Google Play services. Apps relying on the Play Services SDK should check for a compatible APK in both onCreate() and onResume().
  • Web: generate or import a VAPID key pair under the Cloud Messaging tab of the Firebase console; importing requires owner-level project access.

Request permission, then check the APNs token

On iOS 10.4.0+, the APNs token must be available before you make FCM plugin API calls — it isn’t guaranteed to have arrived at startup. The guide’s pattern is permission first, token check second:

// Provisional permission lets iOS show notifications without a prompt.
final notificationSettings =
    await FirebaseMessaging.instance.requestPermission(provisional: true);

// On Apple platforms, confirm the APNs token before any FCM plugin calls.
final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
if (apnsToken != null) {
  // Safe to make FCM plugin API requests.
}

Registration now runs through onRegistered

To target a specific app instance, you need its Firebase Installation ID. The FID is delivered asynchronously via the onRegistered stream, and the guide is explicit about ordering: subscribe before calling register(), so your listener is active when the event fires.

final messaging = FirebaseMessaging.instance;

messaging.onRegistered
    .listen((String installationId) {
      // Send the FID to your application server.
    })
    .onError((err) {
      // Handle registration failure.
    });

// Required on Web, and on Android/Apple if auto-init is disabled.
await messaging.register();

onRegistered emits in three situations: automatically at startup and routine sync on Android and Apple when auto-initialization is enabled (the default); every time a manual register() call finishes (even if already registered); and whenever an FID change triggers re-registration — or, on web, when a pushsubscriptionchange event fires.

Enabling FIDs takes a config flag per platform. On Android, add this inside <application> in AndroidManifest.xml:

<meta-data android:name="firebase_messaging_installation_id_enabled"
           android:value="true" />

On iOS, add FirebaseMessagingInstallationIdEnabled set to true in Info.plist.

The caution in the guide matters: don’t mix the FID APIs with the deprecated registration-token APIs. getToken() and onTokenRefresh still exist — the web variant still requires your VAPID key — but they’re marked for removal, and the recommendation is FIDs for segmentation and targeting.

Controlling auto-initialization

By default, FCM registers the app instance at startup and uploads the identifier and configuration data to Firebase. If you’d rather gate that (consent flows, for example), disable auto-init at build time. On iOS: FirebaseMessagingAutoInitEnabled = NO in Info.plist. On Android you must disable both FCM auto-init and Analytics collection:

<meta-data android:name="firebase_messaging_auto_init_enabled"
           android:value="false" />
<meta-data android:name="firebase_analytics_collection_enabled"
           android:value="false" />

You can re-enable it per app instance at runtime with FirebaseMessaging.instance.setAutoInitEnabled(true) — that value persists across restarts once set. When auto-init is off, call register() yourself (with the onRegistered listener already attached) to register explicitly.

Handling taps: both entry paths

When a user taps a notification, the default behavior on Android and iOS is to open the app — launching it if terminated, foregrounding it if backgrounded. If your notifications carry navigation payloads (a chat message, an order screen), you need to handle both entry states, because they arrive through different APIs: getInitialMessage() returns a RemoteMessage when the app opens from a terminated state (once consumed, it’s removed), while onMessageOpenedApp is a stream for opens from background.

class Application extends StatefulWidget {
  @override
  State<Application> createState() => _ApplicationState();
}

class _ApplicationState extends State<Application> {
  Future<void> setupInteractedMessage() async {
    // Opened from terminated state.
    final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
    if (initialMessage != null) _handleMessage(initialMessage);

    // Opened from background state.
    FirebaseMessaging.instance.onMessageOpenedApp.listen(_handleMessage);
  }

  void _handleMessage(RemoteMessage message) {
    if (message.data['type'] == 'chat') {
      Navigator.pushNamed(context, '/chat', arguments: message);
    }
  }

  @override
  void initState() {
    super.initState();
    setupInteractedMessage(); // initState can't be async.
  }

  @override
  Widget build(BuildContext context) => const Text('...');
}

Note the pattern: data fields drive navigation, and the same handler serves both entry paths so behavior stays consistent.

Test before you build a dashboard

The fastest end-to-end check needs no server code: install and run the app on the target device, accept the permission prompt (on Apple devices), background the app, then create a campaign in the Firebase console (DevOps & Engagement → Messaging) and use Send test message, pasting your FID or registration token. The device should receive the notification while backgrounded. The console’s Reports dashboard then shows sends and opens across Apple and Android, plus impressions for Android.

When you outgrow the console

Console testing covers verification, not delivery. For production you’ll want a server: FCM’s v1 API or an Admin SDK, plus token/FID lifecycle management — the guide links to dedicated docs for registration management, topic messaging, and message priority. The Flutter client side, though, is exactly what’s above: permission, APNs-token check, onRegistered, and both interaction handlers.

Sources