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:
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:
- Firebase Cloud Messaging (FCM) for push notifications
- 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_NOTIFICATIONSpermission 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
.p8key (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
Runnertarget.
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
datapayload 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_notificationsorfirebase_messagingAndroid-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
datapayload. For example:{ "route": "/details", "id": "42", "title": "New Item", "body": "Tap to view details" }. - On tap, parse payload and
Navigator.pushNamedwith 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