Skip to main content
Complete guide to implementing push notifications in Flutter with Zixflow using Firebase Cloud Messaging (FCM) for both iOS and Android.
💡 Track your campaigns: The SDK automatically tracks delivery, opens, and clicks. For advanced tracking customization and implementation details, see Push Notification Tracking.

Prerequisites

Before implementing push notifications:
  1. Configure FCM in Zixflow dashboard - Add FCM Server Key to Zixflow dashboard
  2. Firebase Project Setup - Create Firebase project and add both iOS and Android apps
  3. Device tokens must be associated with users - Call identify() before sending notifications
  4. Test on physical devices only - Emulators/simulators cannot receive push notifications
Important: A device/user can’t receive push notifications until you: (1) Register a device token, (2) Identify a person, (3) Check notification permissions.

Platform-Specific Requirements

iOS Requirements

  • iOS 13.0 or higher
  • Xcode 14.0 or higher
  • Push Notifications capability enabled in Xcode
  • Notification Service Extension for rich push (images, videos)
  • Physical Device required for testing

Android Requirements

  • API Level 21 (Android 5.0) or higher
  • Firebase Project configured
  • google-services.json in android/app/ directory
  • POST_NOTIFICATIONS permission for Android 13+ (API 33+)

Step 1: Firebase Project Setup

1. Create Firebase Project

  1. Go to https://console.firebase.google.com
  2. Click Create a project or select existing project
  3. Follow the setup wizard

2. Add iOS App to Firebase

  1. In Firebase console, click Add app → Select iOS
  2. Enter iOS Bundle ID (e.g., com.yourcompany.app)
  3. Download GoogleService-Info.plist
  4. Add GoogleService-Info.plist to ios/Runner/ directory in Xcode

3. Add Android App to Firebase

  1. In Firebase console, click Add app → Select Android
  2. Enter Android package name (same as iOS bundle ID)
  3. Download google-services.json
  4. Place google-services.json in android/app/ directory

Step 2: Add Dependencies

Update your pubspec.yaml:
dependencies:
  zixflow: ^1.0.0
  firebase_core: ^3.3.0
  firebase_messaging: ^15.0.4
  flutter_local_notifications: ^19.5.0  # For foreground notifications
Install dependencies:
flutter pub get

Step 3: Configure Flutter Code

Initialize Firebase and Zixflow

Update your main.dart:
import 'package:flutter/material.dart';
import 'package:zixflow/zixflow.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'firebase_options.dart';  // Generated by FlutterFire CLI

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase FIRST
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // Configure foreground notification presentation (iOS)
  await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
    alert: true,
    badge: true,
    sound: true,
  );

  // Initialize Zixflow SDK
  final config = ZixflowConfig(
    apiKey: 'YOUR_API_KEY',
    logLevel: LogLevel.debug,
  );
  await Zixflow.initialize(config: config);

  runApp(MyApp());
}
Note: To generate firebase_options.dart, use FlutterFire CLI:
# Install FlutterFire CLI
dart pub global activate flutterfire_cli

# Configure Firebase for your Flutter app
flutterfire configure

Step 4: Handle Push Notifications

Complete push notification handling implementation:
import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:zixflow/zixflow.dart';

// Background message handler (must be top-level function)
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print('Handling background message: ${message.messageId}');
  // Track delivery metric if needed
}

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

  @override
  void initState() {
    super.initState();
    _setupPushNotifications();
  }

  Future<void> _setupPushNotifications() async {
    // Register background message handler
    FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

    // Request permission (iOS)
    await _requestPushPermission();

    // Get FCM token and register with Zixflow
    final token = await FirebaseMessaging.instance.getToken();
    if (token != null) {
      Zixflow.instance.registerDeviceToken(deviceToken: token);
      print('FCM Token: $token');
    }

    // Listen for token refresh
    FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
      Zixflow.instance.registerDeviceToken(deviceToken: newToken);
      print('FCM Token refreshed: $newToken');
    });

    // Handle notification when app is opened from terminated state
    FirebaseMessaging.instance.getInitialMessage().then((message) {
      if (message != null) {
        _handlePushNotificationClick(message, 'terminated');
      }
    });

    // Handle notification when app is in background
    FirebaseMessaging.onMessageOpenedApp.listen((message) {
      _handlePushNotificationClick(message, 'background');
    });

    // Handle notification when app is in foreground
    FirebaseMessaging.onMessage.listen((message) {
      print('Foreground message: ${message.notification?.title}');
      _showForegroundNotification(message);
    });
  }

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

    NotificationSettings settings = await messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
      provisional: false,
    );

    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      print('User granted push notification permission');
    } else if (settings.authorizationStatus == AuthorizationStatus.denied) {
      print('User declined push notification permission');
    } else {
      print('Push notification permission not determined');
    }
  }

  void _handlePushNotificationClick(RemoteMessage message, String appState) {
    print('Push notification clicked - App state: $appState');

    // Track push notification click with Zixflow
    Zixflow.instance.track(
      name: 'push_notification_clicked',
      properties: {
        'push_title': message.notification?.title ?? '',
        'app_state': appState,
        'message_id': message.messageId ?? '',
      },
    );

    // Handle deep link navigation
    if (message.data.containsKey('link')) {
      final deepLink = message.data['link'];
      _navigateToDeepLink(deepLink);
    }
  }

  void _showForegroundNotification(RemoteMessage message) {
    // Display notification when app is in foreground
    // Use flutter_local_notifications package (see Local Notifications section)
  }

  void _navigateToDeepLink(String deepLink) {
    // Implement deep link navigation
    // Example: Navigate to specific screen based on deepLink
    print('Navigating to deep link: $deepLink');
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      home: HomeScreen(),
    );
  }
}

Step 5: Identify Users

Critical: Device tokens must be associated with a user before push notifications can be delivered.
import 'package:zixflow/zixflow.dart';

// Identify user when they log in
Future<void> handleLogin(String userId, String email) async {
  try {
    // Identify user with Zixflow
    Zixflow.instance.identify(
      userId: userId,
      traits: {
        'email': email,
        'first_name': 'John',
        'last_name': 'Doe',
        'plan': 'premium',
      },
    );

    print('User identified: $userId');
  } catch (e) {
    print('Failed to identify user: $e');
  }
}
Important Notes:
  • Device tokens are automatically generated when the app starts
  • Tokens are linked to Zixflow profiles when you call identify()
  • This ensures notifications are delivered to the correct user

iOS-Specific Setup

Step 1: Enable Push Capabilities in Xcode

  1. Open <yourAppName>.xcworkspace in ios/ folder
  2. Select your main app target (Runner)
  3. Go to Signing & Capabilities tab
  4. Click + Capability → Add Push Notifications
  5. Click + Capability → Add Background Modes
  6. Enable Remote notifications under Background Modes

Step 2: Create Notification Service Extension

For rich push notifications (images, videos, GIFs): 1. Create Extension in Xcode:
  1. Select FileNewTarget
  2. Choose Notification Service Extension
  3. Name it NotificationServiceExtension
  4. Click Finish
  5. Click Cancel when asked about activation
2. Configure Podfile: Update ios/Podfile:
target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))

  # Add Zixflow FCM pod
  pod 'zixflow/fcm', :path => '.symlinks/plugins/zixflow/ios'
end

# Notification Service Extension target
target 'NotificationServiceExtension' do
  use_frameworks!
  pod 'zixflow_richpush/fcm', :path => '.symlinks/plugins/zixflow/ios'
end
For Swift Package Manager (Flutter 3.24+, Xcode 16.0+): Enable SPM:
flutter config --enable-swift-package-manager
Remove old pod references from Podfile, then in Xcode:
  1. Add FlutterGeneratedPluginSwiftPackage to NotificationServiceExtension target
  2. Go to GeneralFrameworks and Libraries
3. Install Dependencies:
cd ios
pod install --repo-update
cd ..
4. Update NotificationService.swift:
import UserNotifications
import ZixflowMessagingPushFCM

class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(
        _ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

        // Initialize SDK for extension
        MessagingPushFCM.initializeForExtension(
            withConfig: MessagingPushConfigBuilder(apiKey: "YOUR_API_KEY")
                .build()
        )

        if let bestAttemptContent = bestAttemptContent {
            // Let Zixflow handle rich push and track delivery
            MessagingPush.shared.didReceive(request, withContentHandler: contentHandler)
        }
    }

    override func serviceExtensionTimeWillExpire() {
        // Called when extension is about to be terminated
        MessagingPush.shared.serviceExtensionTimeWillExpire()

        if let contentHandler = contentHandler,
           let bestAttemptContent = bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
}
Important: Replace "YOUR_API_KEY" with your actual API key.

Step 3: Configure AppDelegate.swift

Update ios/Runner/AppDelegate.swift:
import UIKit
import Flutter
import ZixflowMessagingPushFCM
import ZixflowFirebaseWrapper
import FirebaseMessaging
import FirebaseCore

@main
class AppDelegateWitZfIntegration: ZixflowAppDelegateWrapper<AppDelegate> {}

class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)

        // Initialize Zixflow push messaging
        MessagingPushFCM.initialize(
            withConfig: MessagingPushConfigBuilder()
                .showPushAppInForeground(true)  // Show notifications in foreground
                .build()
        )

        // Set notification delegate
        UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    override func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)

        // Register APNs token with Firebase
        Messaging.messaging().apnsToken = deviceToken
    }

    // Optional: Fine-grained control over foreground presentation
    override func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
    ) {
        completionHandler([.banner, .list, .badge, .sound])
    }
}
Foreground Notification Options:
  • Set showPushAppInForeground(true) to display notifications when app is active
  • Customize presentation with userNotificationCenter:willPresent: delegate method
  • Available options: .banner, .list, .badge, .sound
Sound in Notifications:
  • Your app needs permission to play sounds (requested via requestPermission)
  • Users can disable sound in iOS Settings
  • Use “Default” sound setting for best experience (triggers vibration when sound is disabled)
  • Custom sounds require additional native configuration

Android-Specific Setup

Android push is automatically configured when following the Getting Started instructions. Additional customization is available:

Step 1: Configure Gradle

Project-level android/build.gradle:
buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.4.0'
    }
}
App-level android/app/build.gradle:
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'com.google.gms.google-services'  // Add this line
}

android {
    compileSdkVersion 34

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 34
    }
}

Step 2: Add Permissions

Add to android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- Required permissions -->
    <uses-permission android:name="android.permission.INTERNET" />

    <!-- Required for Android 13+ (API 33+) -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

    <application>
        <!-- Your app configuration -->
    </application>
</manifest>

Step 3: Customize Notification Icon and Color

Add to android/app/src/main/AndroidManifest.xml inside <application> tag:
<application>
    <!-- Default notification icon (white, transparent background) -->
    <meta-data
        android:name="com.google.firebase.messaging.default_notification_icon"
        android:resource="@drawable/ic_notification" />

    <!-- Default notification color (accent color) -->
    <meta-data
        android:name="com.google.firebase.messaging.default_notification_color"
        android:resource="@color/colorNotificationIcon" />
</application>
Create notification icon:
  1. Place white icon PNG in android/app/src/main/res/drawable/ic_notification.png
  2. Create multiple sizes for different densities (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi)
Define notification color: Add to android/app/src/main/res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorNotificationIcon">#FF6200EE</color>
</resources>

Local Notifications (Foreground Display)

Show notifications when app is in foreground using flutter_local_notifications:
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

final FlutterLocalNotificationsPlugin localNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future<void> setupLocalNotifications() async {
  // Android settings
  const androidSettings = AndroidInitializationSettings('@drawable/ic_notification');

  // iOS settings
  const iosSettings = DarwinInitializationSettings(
    requestAlertPermission: false,
    requestBadgePermission: false,
    requestSoundPermission: false,
  );

  // Initialization settings
  const initSettings = InitializationSettings(
    android: androidSettings,
    iOS: iosSettings,
  );

  await localNotificationsPlugin.initialize(
    initSettings,
    onDidReceiveNotificationResponse: (response) {
      // Handle local notification click
      Zixflow.instance.track(
        name: 'local_notification_clicked',
        properties: {'payload': response.payload ?? ''},
      );
    },
  );
}

Future<void> showForegroundNotification(RemoteMessage message) async {
  const androidDetails = AndroidNotificationDetails(
    'default_channel',
    'Default Channel',
    channelDescription: 'Default notification channel',
    importance: Importance.high,
    priority: Priority.high,
  );

  const iosDetails = DarwinNotificationDetails(
    presentAlert: true,
    presentBadge: true,
    presentSound: true,
  );

  const details = NotificationDetails(
    android: androidDetails,
    iOS: iosDetails,
  );

  await localNotificationsPlugin.show(
    message.hashCode,
    message.notification?.title ?? 'New Message',
    message.notification?.body ?? '',
    details,
    payload: message.data['link'],
  );
}

Rich Push Payload Structure

Zixflow automatically handles rich push notifications. Here’s how to structure payloads:

iOS FCM Payload

{
  "message": {
    "apns": {
      "payload": {
        "aps": {
          "mutable-content": 1,
          "alert": {
            "title": "Special Offer",
            "body": "Check out our new features!"
          },
          "badge": 1,
          "sound": "default"
        },
        "zixflow": {
          "push": {
            "link": "yourapp://product/123",
            "image": "https://example.com/image.jpg"
          }
        }
      },
      "headers": {
        "apns-priority": 10
      }
    }
  }
}

Android Payload

{
  "message": {
    "data": {
      "title": "Order Shipped",
      "body": "Your order is on the way!",
      "image": "https://example.com/shipping.jpg",
      "link": "yourapp://orders/456"
    }
  }
}

Testing Push Notifications

1. Verify Device Token Registration

Check if device token is registered:
Future<void> verifyPushSetup() async {
  try {
    final token = await FirebaseMessaging.instance.getToken();
    print('FCM Token: $token');

    if (token != null) {
      print('✓ Device token registered');
    } else {
      print('✗ Device token not available');
    }

    final settings = await FirebaseMessaging.instance.getNotificationSettings();
    print('Permission status: ${settings.authorizationStatus}');
  } catch (e) {
    print('Push verification failed: $e');
  }
}

2. Enable Debug Logging

Set log level to debug during development:
final config = ZixflowConfig(
  apiKey: 'YOUR_API_KEY',
  logLevel: LogLevel.debug,  // Verbose logging
);

3. Test on Physical Devices

iOS:
  • Push notifications do not work on iOS Simulator
  • Must use physical iOS device
  • Ensure APNs certificate configured in Firebase
Android:
  • Emulator works if Google Play Services installed
  • Physical device always recommended
  • Ensure FCM Server Key configured in Zixflow dashboard

4. Send Test Push from Zixflow

  1. Log into Zixflow dashboard
  2. Navigate to MessagingPush Notifications
  3. Create test push notification with image
  4. Send to specific user (using userId from identify())

Troubleshooting

Firebase Configuration Issues

Error: GoogleService-Info.plist not found (iOS) Solution:
  1. Ensure file is in ios/Runner/ directory
  2. Verify file is added to Xcode target (check target membership)
  3. Clean and rebuild: flutter clean && flutter pub get

Error: google-services.json not found (Android) Solution:
  1. Ensure file is in android/app/ directory
  2. Verify Google Services plugin applied in android/app/build.gradle
  3. Sync Gradle files

Push Notifications Not Received

iOS Checklist:
  • Push Notifications capability enabled in Xcode
  • APNs certificate uploaded to Firebase
  • Testing on physical device (not simulator)
  • User identified: Zixflow.instance.identify()
  • Permission granted by user
  • Foreground display configured if app is active
Android Checklist:
  • google-services.json in android/app/ directory
  • Google Services plugin applied
  • FCM Server Key in Zixflow dashboard
  • POST_NOTIFICATIONS permission granted (Android 13+)
  • Device registered for push

Rich Push Not Working (Images)

iOS Solutions:
  1. Ensure Notification Service Extension properly configured
  2. Verify extension has correct dependencies in Podfile
  3. Check mutable-content: 1 in push payload
  4. Verify image URL is accessible
  5. Check extension logs for errors
Android Solutions:
  1. Verify image URL accessible from device
  2. Check internet connectivity
  3. Ensure image format supported (JPEG, PNG, GIF)

Permission Denied

Symptoms: User denied push permission Solutions:
  1. Explain value before requesting permission
  2. Show in-app message about benefits
  3. Guide users to Settings to enable manually:
    import 'package:permission_handler/permission_handler.dart';
    
    if (await Permission.notification.isDenied) {
      openAppSettings();
    }
    

Device Token Not Available

Symptoms: getToken() returns null Solutions:
  1. Verify Firebase initialized before calling getToken()
  2. Check Google Play Services available (Android)
  3. Ensure internet connectivity
  4. Review native logs for errors
  5. Rebuild app and retry

Best Practices

  1. Initialize Firebase first - Before Zixflow SDK
  2. Request permission thoughtfully - Explain value before asking
  3. Identify users immediately - After authentication
  4. Test on physical devices - Simulators have limitations
  5. Use rich push - Images increase engagement
  6. Handle deep links - Provide seamless navigation
  7. Monitor metrics - Track delivery and open rates
  8. Enable debug logging - During development only
  9. Test both platforms - iOS and Android behave differently
  10. Clear identity on logout - Call clearIdentify()
  11. Configure App Groups (iOS) - For reliable delivery tracking