All articles
Flutter Push Notifications Complete Guide 2026
Developer Guide15 min read

Flutter Push Notifications: The Complete Setup Guide (2026)

Step-by-step guide to setting up push notifications in Flutter using Firebase Cloud Messaging. Covers Android and iOS config, background handling, FCM topics, and connecting to PushPilot for AI campaigns.

Push notifications are one of the highest-leverage retention tools for Flutter apps. Set them up correctly once, and they work reliably across iOS and Android with a single codebase. Set them up wrong, and you'll spend more time debugging than building.

This guide walks through the complete setup from a blank Firebase project to a working Flutter notification system, including how to connect PushPilot for AI-powered campaigns. Every step is in order, with the code you actually need.

What You'll Need Before Starting

  • A Flutter app (any version from 3.0+)
  • A Firebase account — free at console.firebase.google.com
  • For iOS: a paid Apple Developer account (required to register for APNs)
  • For Android: nothing extra beyond a physical device or emulator with Google Play Services

Note

This guide uses Firebase Cloud Messaging (FCM) for push delivery, which is the most common setup for Flutter apps. FCM handles iOS, Android, and web from a single project.

Firebase Project Setup

If you've already got a Firebase project with your Flutter app registered, skip ahead to the dependencies section. If you're starting fresh:

1

Create a new Firebase project

Go to console.firebase.google.com and click "Add project." Give it a name. You can turn off Google Analytics for now — it's not needed for push notifications.

2

Register your Android app

Click the Android icon. Enter your app's package name (found in android/app/build.gradle as applicationId). Download the google-services.json file and place it in android/app/.

3

Register your iOS app

Click the iOS icon. Enter your Bundle ID (found in Xcode under the Runner target). Download GoogleService-Info.plist and add it to the ios/Runner directory in Xcode (don't just copy the file — use Xcode to add it so it's included in the bundle).

4

Install the FlutterFire CLI (recommended)

Run: dart pub global activate flutterfire_cli then flutterfire configure. This automates the Firebase SDK setup and keeps your configuration in sync.

Add firebase_messaging to Your Flutter App

Add the dependencies to your pubspec.yaml:

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^3.0.0
  firebase_messaging: ^15.0.0

Run:

bash
flutter pub get

Then update your main.dart to initialize Firebase and register the background message handler. The background handler must be a top-level function, not a class method — FCM requires this.

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

// Top-level background handler (must be outside any class)
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print('Background message: ${message.messageId}');
  // Handle your background notification here
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  
  // Register background handler
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  
  runApp(const MyApp());
}

Warning

The @pragma('vm:entry-point') annotation is required for release builds. Without it, tree shaking removes the function and background notifications will be silently ignored.

Android Configuration

FCM on Android requires a couple of configuration steps beyond the pubspec changes.

Add google-services plugin

In android/build.gradle (project-level):

android/build.gradle
buildscript {
  dependencies {
    // Add this line
    classpath 'com.google.gms:google-services:4.4.0'
  }
}

In android/app/build.gradle (app-level), at the bottom:

android/app/build.gradle
apply plugin: 'com.google.gms.google-services'

AndroidManifest.xml

Add the notification permission and FCM service declaration:

AndroidManifest.xml
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest ...>
  <application ...>
    <!-- Required for heads-up notifications on Android 13+ -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    
    <!-- Keep this for FCM to work properly -->
    <service
      android:name="com.google.firebase.messaging.FirebaseMessagingService"
      android:exported="false">
      <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
      </intent-filter>
    </service>
  </application>
</manifest>

Tip

On Android 13+ (API level 33+), users must explicitly grant notification permission. The firebase_messaging package's requestPermission() call handles this automatically — you don't need a separate permission request.

iOS Configuration

iOS push notifications require more setup than Android because Apple's APNs (Apple Push Notification service) requires certificates or keys linked to your Apple Developer account.

Enable Push Notifications capability

In Xcode, select the Runner target. Go to Signing & Capabilities. Click "+ Capability" and add "Push Notifications." Also add "Background Modes" and check "Background fetch" and "Remote notifications."

Create an APNs key in Apple Developer

Go to developer.apple.com. Under Certificates, Identifiers & Profiles, go to Keys. Create a new key with APNs enabled. Download the .p8 file — you can only download it once.

Upload the APNs key to Firebase

In Firebase Console, go to Project Settings > Cloud Messaging > Apple app configuration. Upload the .p8 key file along with your Key ID and Team ID.

Add minimum iOS version

In your ios/Podfile, ensure the minimum iOS version is set to 12.0 or higher: platform :ios, '12.0'

Warning

Push notifications don't work in the iOS Simulator. You'll need a physical iPhone to test iOS notifications. This is an Apple restriction, not a Flutter or FCM limitation.

Handling Notifications in Flutter

Flutter handles push notifications in three states: foreground (app is open), background (app is running in background), and terminated (app is closed). Each state needs separate handling.

Here's the complete notification setup code, typically placed in a NotificationService class or in your main widget's initState:

Step 1: Request permission and get the token

dart
class _NotificationSetup extends State<NotificationSetup> {
  @override
  void initState() {
    super.initState();
    _setupNotifications();
  }

  Future<void> _setupNotifications() async {
    final messaging = FirebaseMessaging.instance;
    
    // Request permission (iOS requires this, Android 13+ requires this)
    final settings = await messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );
    
    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      print('User granted permission');
      
      // Get the FCM token
      final token = await messaging.getToken();
      print('FCM Token: $token');
      
      // Send token to your backend or PushPilot
      await _sendTokenToServer(token!);
    }
  }
}

Step 2: Listen for messages in all app states

dart
void _listenForMessages() {
  // Foreground messages (app is open)
  FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    print('Foreground message: ${message.notification?.title}');
    
    // Show a local notification or update UI
    if (message.notification != null) {
      _showLocalNotification(message.notification!);
    }
  });

  // When user taps a notification that opened the app
  FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
    print('User tapped notification: ${message.data}');
    _handleNotificationTap(message.data);
  });
}

// Check if app was launched from a notification (terminated state)
Future<void> _checkInitialMessage() async {
  final message = await FirebaseMessaging.instance.getInitialMessage();
  if (message != null) {
    _handleNotificationTap(message.data);
  }
}

Tip

The FCM token identifies a specific app installation on a specific device. Store it in your backend and update it whenever the token refreshes. Use messaging.onTokenRefresh to listen for token changes.

Token refresh listener

dart
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
  // Update the token in your backend
  _sendTokenToServer(newToken);
});

FCM Topics for Audience Segmentation

FCM topics let you send a single notification to a group of devices without managing individual tokens. A device subscribes to a topic name, and you can send to all subscribers of that topic with one API call. This is how PushPilot targets user segments.

Subscription and unsubscription happens client-side:

dart
// Subscribe a device to a topic
await FirebaseMessaging.instance.subscribeToTopic('daily_reminders');
await FirebaseMessaging.instance.subscribeToTopic('premium_users');

// Unsubscribe from a topic
await FirebaseMessaging.instance.unsubscribeFromTopic('daily_reminders');

// Example: subscribe on login, unsubscribe on logout
Future<void> onUserLogin(String userId, bool isPremium) async {
  await FirebaseMessaging.instance.subscribeToTopic('all_users');
  if (isPremium) {
    await FirebaseMessaging.instance.subscribeToTopic('premium_users');
  }
}

Topic names can be anything meaningful to your app: all_users, premium_subscribers, fitness_goal_users, daily_active. The convention is lowercase with underscores.

Note

PushPilot uses FCM topics as the targeting mechanism for campaigns. When you create a campaign targeting "all users," PushPilot sends to the FCM topic your app subscribes all devices to on registration. For segment-specific campaigns, create separate topics and subscribe the appropriate users to each one.

Connecting Your Flutter App to PushPilot

Once your Flutter app is sending FCM tokens and subscribing to topics, connecting PushPilot requires zero Flutter code changes. Everything happens at the Firebase project level.

1

Create a PushPilot account

Sign up at pushpilot.ai — the free plan covers 1,000 notifications per month.

2

Generate a Firebase service account key

In Firebase Console, go to Project Settings > Service Accounts. Click "Generate new private key" and download the JSON file.

3

Upload the service account key to PushPilot

In PushPilot, go to Settings > Push Providers > Add Firebase Project. Upload the JSON file. PushPilot now has permission to send notifications through your Firebase project.

4

Create a campaign

Use the conversational campaign builder to describe your campaign. PushPilot will use FCM topics to target your audience and Gemini to generate the notification content.

Testing Your Push Notification Setup

Before running a real campaign, verify the setup is working end-to-end:

Test from Firebase Console

Go to Firebase Console > Cloud Messaging > Send your first message. Send a test message to a specific FCM token (print it from your app logs). This verifies FCM delivery is working.

Test topic delivery

Subscribe your device to a test topic in your app code. Then send a message from Firebase Console to that topic. Verifies topic subscription is working.

Test all three app states

Send notifications while the app is open (foreground), while it's in the background, and after force-closing it (terminated). Each state uses a different code path.

Test from PushPilot

Once connected, create a campaign targeting your test topic and set it to send immediately. This verifies the full chain: PushPilot → Firebase → your device.

Common Issues and Fixes

Notifications work in debug but not release builds

Fix: Missing @pragma('vm:entry-point') annotation on the background handler. In release builds, tree shaking removes functions not referenced directly. The annotation prevents this.

iOS notifications work in sandbox but not production

Fix: Verify you uploaded the correct APNs key to Firebase. The sandbox (development) and production environments use different credentials. Also check that your provisioning profile includes the push notifications entitlement.

FCM token is null on first launch

Fix: On first launch, FCM token generation can take a few seconds. Use a delayed initialization or listen to the tokenRefresh stream rather than calling getToken() immediately in initState.

Topic subscriptions not working

Fix: Topic subscriptions require a valid FCM token. If subscribeToTopic is called before FCM is fully initialized or before the user grants notification permission on Android 13+, it will silently fail. Subscribe after confirming permission and token retrieval.

Background notifications silently dropped on Android

Fix: Background notifications with only a 'data' payload (no 'notification' key) on Android aren't shown as system notifications automatically. You need to handle them in the background handler and show a local notification manually using the flutter_local_notifications package.

Duplicate notifications on Android

Fix: This usually happens when the app receives the same FCM notification in both onMessage (foreground) and via the system tray. The FCM message has a notification payload that Android displays automatically, plus your onMessage handler displaying another one. Remove the local notification display for messages with a notification payload, or only show local notifications for data-only messages.

Frequently Asked Questions

Do I need to change my Flutter app code to use PushPilot?

No. PushPilot connects to your Firebase project and sends notifications through FCM. Your Flutter app receives these notifications exactly the same way it receives any FCM message. The only setup is on the PushPilot side — uploading your Firebase service account key.

Can Flutter push notifications work without the app being open?

Yes. Push notifications work when the app is in the background or fully closed. Background handling uses the _firebaseMessagingBackgroundHandler function. For the terminated state, FirebaseMessaging.instance.getInitialMessage() gives you the notification that launched the app.

How do I handle deep linking from push notifications in Flutter?

Include a data payload in your FCM message with a 'route' or 'url' key. In your notification tap handlers (onMessage, onMessageOpenedApp, getInitialMessage), read the data payload and navigate accordingly. The go_router package works well with this pattern.

What's the difference between FCM data messages and notification messages?

Notification messages have a 'notification' key and are displayed automatically by the OS when the app is in the background or terminated. Data messages have only a 'data' key and are always delivered to your handler — you control the display. Use notification messages for simple notifications, data messages when you need custom UI or silent background processing.

Do Flutter push notifications work on all Android versions?

FCM push notifications work on Android 4.4+ (KitKat) and above. On Android 13+ (API 33+), you need to request the POST_NOTIFICATIONS permission — the firebase_messaging package handles this automatically when you call requestPermission(). For older Android versions, the permission isn't needed.

Ready to automate your push notifications?

Start sending AI-powered push notifications in under 5 minutes. Free plan available — no credit card required.

Get Started Free

More from the blog