Flutter

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.

Dart
# 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)
# }
Dart
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.

Dart
// 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}',
);
Dart
// 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.

Dart
# 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'
Dart
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().

Dart
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.

Dart
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');
}