← Back to list

Build Push Notifications (FCM) and Analytics into Your Flutter App: A Practical, Step‑by‑Step Guide

Shipping push notifications and measuring user behavior are table stakes for modern apps. In this guide you’ll integrate both:

Moyeen Haider · 2025-09-20 13:22 · 0 claps · 6.5 min read
#flutter-app-development #push-notification #firebaseanalytics #local-notifications
Open on Medium ↗
Wiki topics: GRW · Growth & Analytics 📱 · Mobile Development

Build Push Notifications (FCM) and Analytics into Your Flutter App: A Practical, Step‑by‑Step Guide

Shipping push notifications and measuring user behavior are table stakes for modern apps. In this guide you’ll integrate both:

  1. Firebase Cloud Messaging (FCM) for push notifications
  2. Firebase Analytics for event tracking and insights

We’ll start with FCM, then add Analytics. You’ll get platform-specific notes for iOS along the way.

What you’ll build:

  • Receive push notifications in foreground, background, and terminated states
  • Show local notifications when the app is foregrounded
  • Handle notification taps to navigate
  • Retrieve and display the device’s FCM token
  • Log analytics screen views, custom events, set user IDs and properties
  • Test with DebugView (Android and iOS)

Prereqs:

  • Flutter installed and working
  • A Firebase project
  • Xcode for iOS and an Apple Developer account (to enable APNs on real devices)

Note: Code examples assume Flutter with Dart 3+ and the following packages: firebase_core, firebase_messaging, flutter_local_notifications, and firebase_analytics.

Part 1 — Firebase Cloud Messaging (Push Notifications)

1) Set up Firebase in your Flutter app

  • Create a Firebase project in the Firebase console.
  • Add Android and iOS apps to the project.
  • Configure FlutterFire:
flutter pub add firebase_core firebase_messaging flutter_local_notifications
dart run flutterfire_cli:flutterfire configure

This generates firebase_options.dart and configures platforms you select. On Android, make sure google-services.json is in app.

Important Android Gradle setup:

  • Ensure Google Services plugin is applied in android/app/build.gradle(.kts).
  • If you run into Java 8/Desugaring method errors, enable desugaring and add the desugar dependency:
  • compileOptions { coreLibraryDesugaringEnabled true }
  • dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:<latest>' }

iOS:

  • GoogleService-Info.plist must exist (FlutterFire handles this).
  • We’ll configure APNs in a later step.

2) Initialize Firebase and FCM

In main.dart, initialize Firebase early and set up your notification services. Also register the background handler for FCM messages.

Example:

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'services/messaging_service.dart';
import 'services/local_notifications_service.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  // Local notifications init (handles tap payloads)
  await LocalNotificationsService.instance.init(onSelect: (payloadJson) async {
    // Decode payload; navigate accordingly.
    // Example: use NavigatorKey to push to details route.
  });

  // Register background handler
  FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);

  runApp(const MyApp());
}

3) Create a MessagingService

Encapsulate permission requests, token retrieval, and message listeners. Make sure to set iOS foreground presentation options so you can see notifications while the app is open.

// services/messaging_service.dart
import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'local_notifications_service.dart';

class MessagingService {
  MessagingService._();
  static final instance = MessagingService._();
  final _fm = FirebaseMessaging.instance;

  Future<NotificationSettings> requestPermission() async {
    return _fm.requestPermission(alert: true, badge: true, sound: true);
  }

  Future<String?> getToken() => _fm.getToken();

  Future<void> initListeners() async {
    // iOS foreground presentation
    await _fm.setForegroundNotificationPresentationOptions(
      alert: true, badge: true, sound: true,
    );

    FirebaseMessaging.onMessage.listen((message) {
      final title = message.notification?.title ?? message.data['title'];
      final body = message.notification?.body ?? message.data['body'];
      // Show local notification in foreground
      LocalNotificationsService.instance.show(
        id: DateTime.now().millisecondsSinceEpoch.remainder(100000),
        title: title ?? 'Message',
        body: body ?? '',
        payload: jsonEncode(message.data),
      );
    });

    FirebaseMessaging.onMessageOpenedApp.listen((message) {
      // Handle tap from background
      final payload = jsonEncode(message.data);
      LocalNotificationsService.instance.handleTap(payload);
    });

    final initial = await _fm.getInitialMessage();
    if (initial != null) {
      // Handle tap from terminated state
      LocalNotificationsService.instance.handleTap(jsonEncode(initial.data));
    }

    FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
      // Optionally send to your backend
    });
  }
}

// Top-level background handler
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // If Firebase not initialized in background isolate, initialize it.
  // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  final data = message.data;
  await LocalNotificationsService.instance.show(
    id: DateTime.now().millisecondsSinceEpoch.remainder(100000),
    title: data['title'] ?? 'Background Message',
    body: data['body'] ?? '',
    payload: jsonEncode(data),
  );
}

Call MessagingService.instance.initListeners() after Firebase initialization (e.g., in main() after LocalNotifications init).

4) Add Local Notifications

We’ll use flutter_local_notifications to display notifications while the app is foregrounded and handle taps consistently.

// services/local_notifications_service.dart
import 'dart:convert';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

typedef OnSelect = Future<void> Function(String payloadJson);

class LocalNotificationsService {
  LocalNotificationsService._();
  static final instance = LocalNotificationsService._();
  final _plugin = FlutterLocalNotificationsPlugin();
  OnSelect? _onSelect;

  Future<void> init({OnSelect? onSelect}) async {
    _onSelect = onSelect;
    const android = AndroidInitializationSettings('@mipmap/ic_launcher');
    final ios = DarwinInitializationSettings(
      onDidReceiveLocalNotification: (id, title, body, payload) async {},
    );
    final settings = InitializationSettings(android: android, iOS: ios);
    await _plugin.initialize(
      settings,
      onDidReceiveNotificationResponse: (resp) async {
        final payload = resp.payload;
        if (payload != null && _onSelect != null) {
          await _onSelect!(payload);
        }
      },
    );
  }

  Future<void> show({
    required int id,
    required String title,
    required String body,
    String? payload,
  }) async {
    const androidDetails = AndroidNotificationDetails(
      'default_channel',
      'General',
      importance: Importance.defaultImportance,
      priority: Priority.defaultPriority,
    );
    const iosDetails = DarwinNotificationDetails();
    final details = NotificationDetails(android: androidDetails, iOS: iosDetails);
    await _plugin.show(id, title, body, details, payload: payload);
  }

  // Used by Messaging to simulate a tap via payload
  Future<void> handleTap(String payload) async {
    // e.g., parse route and navigate
    final data = jsonDecode(payload) as Map<String, dynamic>;
    // Use navigatorKey.currentState?.pushNamed(data['route'], arguments: {'payload': data});
  }

  Future<void> requestAndroidPermission() async {
    // Android 13+ runtime permission
    try {
      await _plugin
          .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
          ?.requestPermission();
    } catch (_) {}
  }

  Future<bool?> areAndroidNotificationsEnabled() {
    return _plugin
        .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
        ?.areNotificationsEnabled();
  }
}

Android manifest setup:

  • Add POST_NOTIFICATIONS permission for Android 13+:
<!-- AndroidManifest.xml inside <manifest> -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
  • Provide a default channel id if needed via metadata, or rely on the channel defined in code.

5) iOS-specific configuration (APNs)

Push notifications on iOS require APNs:

In the Apple Developer portal:

  • Create an APNs Authentication Key (recommended) or certificates.
  • Download the .p8 key (if using key) and note Key ID and Team ID.

In Firebase console → Project Settings → iOS app:

  • Upload the APNs key and fill Key ID and Team ID.

In Xcode:

  • Enable Push Notifications and Background Modes → “Remote notifications” for the Runner target.

In Info.plist, include usage strings if you also request alert/sound/badge permissions (FlutterFire handles the prompt via requestPermission()).

Test on a physical device; the iOS simulator does not receive remote APNs pushes.

Foreground presentation:

We enabled it via setForegroundNotificationPresentationOptions(alert: true, badge: true, sound: true). Without this, iOS will not show notifications while the app is in the foreground.

6) Test FCM end-to-end

  • At runtime, request permission and fetch a token:
final settings = await MessagingService.instance.requestPermission();
final token = await MessagingService.instance.getToken();
// Show token in UI; add a “Copy” button for convenience.
  • Send a test message from Firebase console → Cloud Messaging → “Send test message”.

Paste the device’s FCM token. Use a data payload to control navigation:

{
  "route": "/details",
  "id": "42",
  "title": "New Item",
  "body": "Tap to view details"
}
  • Foreground: You should see a local notification.
  • Background/Terminated: Tapping the notification should navigate into your app and handle the payload (e.g., push to a details screen).

Troubleshooting quick hits (more later):

  • Android 13+: ensure runtime permission for notifications is granted.
  • iOS: ensure APNs setup is complete and you’re on a real device.
  • Check you’re using data payload fields consistently (title, body, route, etc.).

Part 2 — Firebase Analytics

With notifications working, let’s add Analytics to measure usage.

1) Install and initialize

Initialize Firebase (already done in Part 1). Create an AnalyticsService to centralize behavior and validation:

// services/analytics_service.dart
import 'dart:async';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart';

class AnalyticsService {
  AnalyticsService._();
  static final instance = AnalyticsService._();

  final analytics = FirebaseAnalytics.instance;
  FirebaseAnalyticsObserver get observer => FirebaseAnalyticsObserver(analytics: analytics);

  Future<void> initDefaults(FirebaseOptions options) async {
    // Optional: ensure collection is enabled; set default params as needed
    await analytics.setAnalyticsCollectionEnabled(true);
    // You can set default params like project/app IDs if desired
  }

  // Simple name validator to avoid silent drops
  bool _isValidEventName(String name) {
    final reg = RegExp(r'^[a-zA-Z][a-zA-Z0-9_]{0,39}$'); // Firebase event naming rules
    return reg.hasMatch(name);
  }

  Future<void> logEvent(String name, Map<String, Object?> params) async {
    try {
      if (!_isValidEventName(name)) {
        await analytics.logEvent(name: 'app_error', parameters: {
          'context': 'analytics',
          'reason': 'invalid_event_name',
          'supplied_name': name,
        });
        return;
      }
      final cleaned = Map<String, Object?>.fromEntries(
        params.entries.where((e) => e.value != null),
      );
      await analytics.logEvent(name: name, parameters: cleaned);
    } catch (e) {
      await analytics.logEvent(name: 'app_error', parameters: {
        'context': 'analytics',
        'reason': 'log_event_exception',
        'message': e.toString(),
      });
    }
  }

  Future<void> logScreenView({required String screenName}) {
    return analytics.logScreenView(screenName: screenName);
  }
}

In main.dart, add the observer to MaterialApp and initialize defaults:

MaterialApp(
  navigatorObservers: [AnalyticsService.instance.observer],
  // routes...
);

Call AnalyticsService.instance.initDefaults(DefaultFirebaseOptions.currentPlatform); after Firebase initialization.

2) Log screen views and custom events

Screen views:

AnalyticsService.instance.logScreenView(screenName: 'Home');

Custom events:

await AnalyticsService.instance.logEvent('tutorial_begin', {
  'step': 1,
  'context': 'home_screen',
});

Event naming rules (enforced by the validator above):

  • Start with a letter
  • Only letters, numbers, and underscore
  • Max 40 characters

Setting user identity:

await AnalyticsService.instance.analytics.setUserId(id: '12345');
await AnalyticsService.instance.analytics.setUserProperty(name: 'plan_tier', value: 'pro');

3) Test with DebugView

Android (device or emulator):

Enable DebugView:

adb shell setprop debug.firebase.analytics.app com.your.package

Relaunch the app.

Open Firebase Console → Analytics → DebugView.

iOS (simulator or device):

Run the app; open Firebase Console → Analytics → DebugView.

No ADB command is needed.

Expect a short delay for some events; DebugView typically shows them quickly, but production dashboards can take hours.

Troubleshooting & Tips

Android 13+ notifications:

  • You must request runtime permission before notifications show. Use flutter_local_notifications or firebase_messaging Android-specific APIs to request it. Provide an in-app button: “Request Notification Permission (Android 13+)”.

iOS push not arriving:

  • Must use a physical device (not the simulator) for APNs.
  • Ensure APNs key uploaded in Firebase console and Push capability enabled in Xcode.
  • Foreground notifications require explicit presentation options (alert/sound/badge).

Token issues:

  • Tokens can refresh — listen to FirebaseMessaging.instance.onTokenRefresh.
  • Show the token in-app and provide a “Copy” button for quick testing.

Local notifications not showing:

  • Confirm channel configuration on Android and that runtime permission is granted on Android 13+.

Payload routing:

  • Use a consistent data payload. For example: { "route": "/details", "id": "42", "title": "New Item", "body": "Tap to view details" }.
  • On tap, parse payload and Navigator.pushNamed with arguments.

Analytics event not visible:

  • Ensure event name follows rules.
  • Use DebugView during development.
  • Confirm setAnalyticsCollectionEnabled(true) if you previously disabled it.

Putting it all together

  • Initialize Firebase, Local Notifications, and FCM listeners at startup.
  • Request notification permissions and display token in-app.
  • Show notifications when messages arrive in foreground using local notifications.
  • Handle taps (background/terminated) and navigate accordingly.
  • Add Analytics observer, log screen views, and log custom events with validation.
  • Test with Firebase Console (FCM) and DebugView (Analytics).

Optional UI polish ideas:

  • Group messaging sections into cards: “How to Test,” “Troubleshooting,” “Device & Permissions,” “Last Payload,” “Actions.”
  • Provide buttons: “Refresh Token,” “Copy Token,” “Request Android 13 Permission,” “Send Local Test Notification.”

Conclusion

You now have a solid, production-oriented foundation for push notifications and analytics:

  • FCM integrates messaging across app states with local notifications for foreground display.
  • Firebase Analytics tracks screen views, custom events, and user properties, with DebugView for quick validation.

From here, you can:

  • Send targeted notifications using topics or conditionals.
  • Add deeper analytics funnels and user segmentation.
  • Wire tokens and events to your backend for personalized experiences.

메타데이터
post_id
31064d665ddd
slug
build-push-notifications-fcm-and-analytics-into-your-flutter-app-a-practical-step-by-step-guide-31064d665ddd
url
https://medium.com/@moyeen_haider/build-push-notifications-fcm-and-analytics-into-your-flutter-app-a-practical-step-by-step-guide-31064d665ddd
canonical_url
https://medium.com/@moyeen_haider/build-push-notifications-fcm-and-analytics-into-your-flutter-app-a-practical-step-by-step-guide-31064d665ddd
author_url
https://medium.com/@moyeen_haider
status
ok
fetched_at
2026-08-19 22:55:36