Notifications
flutter_local_notifications setup, scheduling local notifications, FCM (firebase_messaging) setup, handling foreground and background messages, and notification permissions.
flutter_local_notifications – Setup
Add the dependency, configure Android channels, and initialize the plugin at app startup before using any notification features.
# pubspec.yaml
dependencies:
flutter_local_notifications: ^17.0.0
# Android: android/app/src/main/AndroidManifest.xml
# (Add inside <manifest>)
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
# iOS: ios/Runner/AppDelegate.swift
# FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { registry in
# GeneratedPluginRegistrant.register(with: registry)
# }
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future<void> initLocalNotifications() async {
// Android init settings
const AndroidInitializationSettings androidSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
// iOS / macOS init settings
const DarwinInitializationSettings darwinSettings =
DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const InitializationSettings initSettings = InitializationSettings(
android: androidSettings,
iOS: darwinSettings,
);
await flutterLocalNotificationsPlugin.initialize(
initSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) {
// Handle notification tap
print('Notification tapped: ${response.payload}');
},
);
// Create Android notification channel
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'main_channel', // id
'Main Notifications', // name
description: 'General app notifications',
importance: Importance.high,
);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
}
// Call in main()
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initLocalNotifications();
runApp(const MyApp());
}
Show & Schedule Local Notifications
Use show() for immediate notifications and zonedSchedule() for time-based scheduling. Import timezone for scheduled notifications.
// Immediate notification
Future<void> showNotification({
required int id,
required String title,
required String body,
String? payload,
}) async {
const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
'main_channel',
'Main Notifications',
channelDescription: 'General app notifications',
importance: Importance.high,
priority: Priority.high,
icon: '@mipmap/ic_launcher',
largeIcon: DrawableResourceAndroidBitmap('@mipmap/ic_launcher'),
styleInformation: BigTextStyleInformation(''),
);
const NotificationDetails details = NotificationDetails(
android: androidDetails,
iOS: DarwinNotificationDetails(badgeNumber: 1),
);
await flutterLocalNotificationsPlugin.show(id, title, body, details, payload: payload);
}
// Usage
await showNotification(
id: 1,
title: 'New Message',
body: 'You have a new message from Alice',
payload: '{"route": "/messages", "id": 42}',
);
// pubspec.yaml – also add:
// timezone: ^0.9.0
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
// Initialize timezone in main()
tz.initializeTimeZones();
// Schedule a notification 5 minutes from now
Future<void> scheduleNotification() async {
final scheduledDate = tz.TZDateTime.now(tz.local).add(const Duration(minutes: 5));
await flutterLocalNotificationsPlugin.zonedSchedule(
2, // notification id
'Reminder',
'Your timer has expired!',
scheduledDate,
const NotificationDetails(
android: AndroidNotificationDetails(
'main_channel', 'Main Notifications',
importance: Importance.high,
priority: Priority.high,
),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
payload: 'timer_done',
);
}
// Cancel a specific notification
await flutterLocalNotificationsPlugin.cancel(2);
// Cancel all notifications
await flutterLocalNotificationsPlugin.cancelAll();
FCM Setup (firebase_messaging)
Add Firebase to your project, initialize the app, and set up FCM. Retrieve the device token to send targeted push notifications from your backend.
# pubspec.yaml
dependencies:
firebase_core: ^3.0.0
firebase_messaging: ^15.0.0
# Terminal
# flutterfire configure <- sets up google-services.json & GoogleService-Info.plist
# android/app/build.gradle – ensure google-services plugin applied
# apply plugin: 'com.google.gms.google-services'
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
// Background message handler – must be top-level function
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print('Background message: ${message.messageId}');
}
Future<void> initFCM() async {
await Firebase.initializeApp();
// Register background handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
final messaging = FirebaseMessaging.instance;
// Get FCM token
final token = await messaging.getToken();
print('FCM Token: $token');
// Token refresh listener
messaging.onTokenRefresh.listen((newToken) {
print('Token refreshed: $newToken');
// Send newToken to your backend
});
// Subscribe to a topic
await messaging.subscribeToTopic('breaking_news');
}
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initFCM();
runApp(const MyApp());
}
Handling Foreground & Background Messages
FCM delivers messages differently based on app state. Handle foreground messages in-app, background via the top-level handler, and terminated via getInitialMessage().
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_setupFCMListeners();
}
void _setupFCMListeners() {
// 1. Foreground message (app is open)
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
final notification = message.notification;
if (notification != null) {
print('Foreground – Title: ${notification.title}');
print('Foreground – Body: ${notification.body}');
// Show in-app banner or SnackBar instead of system notification
_showInAppBanner(notification.title!, notification.body!);
}
// Access data payload
print('Data: ${message.data}');
});
// 2. Notification tap when app is in background
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print('App opened from background via notification');
_handleNotificationTap(message.data);
});
// 3. Notification tap when app was terminated
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) {
print('App launched from terminated state via notification');
_handleNotificationTap(message.data);
}
});
}
void _handleNotificationTap(Map<String, dynamic> data) {
final route = data['route'] as String?;
if (route != null) {
Navigator.pushNamed(context, route);
}
}
void _showInAppBanner(String title, String body) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$title: $body')),
);
}
@override
Widget build(BuildContext context) => const MaterialApp(home: HomePage());
}
Notification Permissions
On iOS and Android 13+, you must explicitly request permission before sending notifications. Check the current status and handle denied cases gracefully.
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:permission_handler/permission_handler.dart'; // optional helper
// Request FCM notification permission (iOS + Android 13+)
Future<void> requestNotificationPermission() async {
final messaging = FirebaseMessaging.instance;
final settings = await messaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
switch (settings.authorizationStatus) {
case AuthorizationStatus.authorized:
print('User granted permission');
break;
case AuthorizationStatus.provisional:
print('User granted provisional permission');
break;
case AuthorizationStatus.denied:
print('User denied permission');
// Optionally guide user to settings
openAppSettings(); // from permission_handler
break;
case AuthorizationStatus.notDetermined:
print('Permission not yet requested');
break;
}
}
// Check current permission status
Future<AuthorizationStatus> checkPermissionStatus() async {
final settings = await FirebaseMessaging.instance.getNotificationSettings();
return settings.authorizationStatus;
}
// For flutter_local_notifications on Android 13+
Future<void> requestLocalNotificationPermission() async {
final androidPlugin = flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
final granted = await androidPlugin?.requestNotificationsPermission();
print('Android notification permission granted: $granted');
}