image

25 Mar 2026

9K

35K

Building Advanced Notification Center Widgets in Flutter: Grouping, Swipe, and Extended Actions

Modern mobile applications often rely on robust notification systems to keep users informed and engaged. Beyond simple alerts, rich notifications that offer grouping, interactive swipe actions, and extended action buttons significantly enhance the user experience. This article will guide you through creating such sophisticated Notification Center widgets in Flutter using the flutter_local_notifications plugin.

1. Getting Started: The Flutter Local Notifications Plugin

The flutter_local_notifications plugin is your go-to solution for displaying local notifications across platforms (Android, iOS, macOS, Linux, Windows). It provides a unified API to manage notification channels, display notifications, and handle user interactions.

1.1. Installation

First, add the plugin to your pubspec.yaml file:


dependencies:
  flutter:
    sdk: flutter
  flutter_local_notifications: ^17.1.2 # Use the latest version
  timezone: ^0.9.3 # Required for scheduled notifications, good to include
  flutter_timezone: ^1.0.1 # Required for scheduled notifications

Run flutter pub get to fetch the dependencies.

1.2. Basic Initialization

Before showing any notifications, you must initialize the plugin. This typically happens once, early in your application's lifecycle (e.g., in main() or a splash screen).


import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:flutter_timezone/flutter_timezone.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future initializeNotifications() async {
  tz.initializeTimeZones();
  final String timeZoneName = await FlutterTimezone.getLocalTimezone();
  tz.setLocalLocation(tz.getLocation(timeZoneName));

  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('app_icon'); // 'app_icon' refers to a drawable resource name

  final DarwinInitializationSettings initializationSettingsDarwin =
      DarwinInitializationSettings(
    requestAlertPermission: true,
    requestBadgePermission: true,
    requestSoundPermission: true,
    onDidReceiveLocalNotification: (int id, String? title, String? body, String? payload) async {
      // Handle notification when app is in foreground (iOS < 10)
      // Usually, you might show a dialog or update UI
    },
  );

  final InitializationSettings initializationSettings = InitializationSettings(
    android: initializationSettingsAndroid,
    iOS: initializationSettingsDarwin,
    macOS: initializationSettingsDarwin,
  );

  await flutterLocalNotificationsPlugin.initialize(
    initializationSettings,
    onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async {
      // Handle user tapping on notification or action button
      // This is for Android, iOS 10+, macOS
      if (notificationResponse.payload != null) {
        print('notification payload: ${notificationResponse.payload}');
      }
      switch (notificationResponse.notificationResponseType) {
        case NotificationResponseType.selectedNotification:
          // User tapped on the notification itself
          print('Notification tapped: ${notificationResponse.id}');
          break;
        case NotificationResponseType.selectedNotificationAction:
          // User tapped on an action button
          print('Action button tapped: ${notificationResponse.actionId}');
          break;
      }
    },
    onDidReceiveBackgroundNotificationResponse: (NotificationResponse notificationResponse) async {
      // Handle background notification responses (Android only, requires specific setup)
      print('Background notification response: ${notificationResponse.actionId}');
    },
  );
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeNotifications();
  runApp(const MyApp());
}

Remember to add an app icon in android/app/src/main/res/drawable/app_icon.png (or any other drawable name you prefer) for the Android initialization.

2. Showing a Simple Notification

Before diving into advanced features, let's see how to display a basic notification.


Future showBasicNotification() async {
  const AndroidNotificationDetails androidNotificationDetails =
      AndroidNotificationDetails(
    'basic_channel_id',
    'Basic Notifications',
    channelDescription: 'Channel for basic notifications',
    importance: Importance.max,
    priority: Priority.high,
    ticker: 'ticker',
  );

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

  const NotificationDetails platformChannelSpecifics = NotificationDetails(
    android: androidNotificationDetails,
    iOS: darwinNotificationDetails,
    macOS: darwinNotificationDetails,
  );

  await flutterLocalNotificationsPlugin.show(
    0, // Notification ID
    'Simple Title',
    'This is a simple notification body.',
    platformChannelSpecifics,
    payload: 'item_id_123',
  );
}

3. Implementing Notification Grouping

Grouping notifications helps to declutter the Notification Center, presenting multiple related notifications as a single, summarized entry. Tapping the summary typically expands it to show individual notifications.

3.1. Android Grouping

Android utilizes groupKey, setAsGroupSummary, and groupAlertBehavior within AndroidNotificationDetails.

  • groupKey: A string key that identifies the group. All notifications with the same key will be grouped.
  • setAsGroupSummary: Set to true for the notification that acts as the summary for the group. This notification often has a different title/body summarizing the group.
  • groupAlertBehavior: Defines how alerts are handled when notifications are grouped.

Future showGroupedNotifications() async {
  const String groupKey = 'com.example.app.WORK_EMAIL';
  const String groupChannelId = 'grouped_channel_id';
  const String groupChannelName = 'Grouped Notifications';
  const String groupChannelDescription = 'Channel for grouped notifications';

  // Android: Create a notification channel for the group
  const AndroidNotificationChannel androidGroupChannel = AndroidNotificationChannel(
    groupChannelId,
    groupChannelName,
    description: groupChannelDescription,
    importance: Importance.max,
  );
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          AndroidFlutterLocalNotificationsPlugin>()
      ?.createNotificationChannel(androidGroupChannel);

  // 1. Individual notification (child 1)
  final AndroidNotificationDetails firstNotificationDetails =
      AndroidNotificationDetails(
    groupChannelId,
    groupChannelName,
    channelDescription: groupChannelDescription,
    groupKey: groupKey,
    importance: Importance.max,
    priority: Priority.high,
  );
  await flutterLocalNotificationsPlugin.show(
    1,
    'New Email from John Doe',
    'Subject: Project Update',
    NotificationDetails(android: firstNotificationDetails),
    payload: 'email_1',
  );

  // 2. Individual notification (child 2)
  final AndroidNotificationDetails secondNotificationDetails =
      AndroidNotificationDetails(
    groupChannelId,
    groupChannelName,
    channelDescription: groupChannelDescription,
    groupKey: groupKey,
    importance: Importance.max,
    priority: Priority.high,
  );
  await flutterLocalNotificationsPlugin.show(
    2,
    'New Email from Jane Smith',
    'Subject: Meeting Reminder',
    NotificationDetails(android: secondNotificationDetails),
    payload: 'email_2',
  );

  // 3. Group summary notification
  final AndroidNotificationDetails groupSummaryNotificationDetails =
      AndroidNotificationDetails(
    groupChannelId,
    groupChannelName,
    channelDescription: groupChannelDescription,
    groupKey: groupKey,
    setAsGroupSummary: true,
    groupAlertBehavior: GroupAlertBehavior.children,
    // Style information for the summary
    styleInformation: InboxStyleInformation(
      [
        'John Doe: Project Update',
        'Jane Smith: Meeting Reminder',
      ],
      contentTitle: '2 New Emails',
      summaryText: 'You have 2 new work emails',
    ),
    importance: Importance.max,
    priority: Priority.high,
  );
  await flutterLocalNotificationsPlugin.show(
    3, // Must be different from child IDs
    '2 New Emails',
    'You have 2 new work emails',
    NotificationDetails(android: groupSummaryNotificationDetails),
    payload: 'email_group_summary',
  );
}

3.2. iOS/macOS Grouping

iOS/macOS uses threadIdentifier within DarwinNotificationDetails (or iOSNotificationDetails for older versions) to group notifications. Notifications with the same threadIdentifier will be grouped in the Notification Center.


Future showGroupedNotificationsIos() async {
  const String threadId = 'com.example.app.MESSAGE_THREAD';

  final DarwinNotificationDetails firstDarwinNotificationDetails =
      DarwinNotificationDetails(
    threadIdentifier: threadId,
    presentAlert: true,
    presentBadge: true,
    presentSound: true,
  );

  await flutterLocalNotificationsPlugin.show(
    4,
    'Alice',
    'Hey, how are you?',
    NotificationDetails(iOS: firstDarwinNotificationDetails, macOS: firstDarwinNotificationDetails),
    payload: 'message_alice_1',
  );

  final DarwinNotificationDetails secondDarwinNotificationDetails =
      DarwinNotificationDetails(
    threadIdentifier: threadId,
    presentAlert: true,
    presentBadge: true,
    presentSound: true,
  );

  await flutterLocalNotificationsPlugin.show(
    5,
    'Bob',
    'Are we still on for tomorrow?',
    NotificationDetails(iOS: secondDarwinNotificationDetails, macOS: secondDarwinNotificationDetails),
    payload: 'message_bob_1',
  );
}

4. Handling Swipe and User Interaction

Swipe actions (like dismissing a notification) are primarily managed by the operating system's Notification Center. However, we can configure how our notifications behave when interacted with.

4.1. Tapping a Notification

When a user taps on a notification, the onDidReceiveNotificationResponse callback (registered during initialization) is triggered. This is where you route the user to specific content within your app.


// Inside initializeNotifications() or wherever you handle notification responses
onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async {
  if (notificationResponse.payload != null) {
    print('Notification payload received: ${notificationResponse.payload}');
    // Example: Navigate to a specific screen based on payload
    // Navigator.push(
    //   context,
    //   MaterialPageRoute(builder: (context) => DetailScreen(payload: notificationResponse.payload!)),
    // );
  }
  // ... handle other response types if needed
},

5. Adding Extended Action Buttons

Extended action buttons allow users to perform quick actions directly from the notification without opening the app, greatly enhancing productivity.

5.1. Android Extended Actions

For Android, define AndroidNotificationAction instances and pass them in a list to the actions property of AndroidNotificationDetails.


Future showNotificationWithActions() async {
  const String channelId = 'action_channel_id';
  const String channelName = 'Action Notifications';
  const String channelDescription = 'Channel with action buttons';

  // Define notification actions
  final List actions = [
    const AndroidNotificationAction(
      'reply_action',
      'Reply',
      showsUserInterface: true, // For inline reply
      // This icon is optional and might not be displayed depending on Android version
      // icon: DrawableResourceAndroidBitmap('reply_icon'),
      contextual: true, // Show this action prominently
    ),
    const AndroidNotificationAction(
      'archive_action',
      'Archive',
      // icon: DrawableResourceAndroidBitmap('archive_icon'),
    ),
    const AndroidNotificationAction(
      'delete_action',
      'Delete',
      // icon: DrawableResourceAndroidBitmap('delete_icon'),
      cancelNotification: true, // Dismiss notification after action
    ),
  ];

  final AndroidNotificationDetails androidNotificationDetails =
      AndroidNotificationDetails(
    channelId,
    channelName,
    channelDescription: channelDescription,
    importance: Importance.max,
    priority: Priority.high,
    actions: actions,
  );

  await flutterLocalNotificationsPlugin.show(
    6,
    'Message from Sarah',
    'Hey, did you get my last email?',
    NotificationDetails(android: androidNotificationDetails),
    payload: 'message_from_sarah',
  );
}

// Handling action button presses (in onDidReceiveNotificationResponse)
// ...
// case NotificationResponseType.selectedNotificationAction:
//   print('Action button tapped: ${notificationResponse.actionId}');
//   if (notificationResponse.actionId == 'reply_action') {
//     print('Reply to: ${notificationResponse.payload}');
//     // If showsUserInterface was true, notificationResponse.input might contain user input
//     if (notificationResponse.input != null && notificationResponse.input!.isNotEmpty) {
//       print('User replied: ${notificationResponse.input}');
//     }
//   } else if (notificationResponse.actionId == 'archive_action') {
//     print('Archiving: ${notificationResponse.payload}');
//   } else if (notificationResponse.actionId == 'delete_action') {
//     print('Deleting: ${notificationResponse.payload}');
//   }
//   break;
// ...

5.2. iOS/macOS Extended Actions

iOS/macOS uses a concept called "Notification Categories" and "Notification Actions". You define categories, and within each category, you define actions. When a notification with a specific categoryIdentifier is shown, the OS presents the associated actions.


// Define Notification Categories during initialization
Future initializeNotificationsWithCategories() async {
  // ... existing initialization code

  // iOS/macOS specific setup for action categories
  final DarwinNotificationAction replyAction = DarwinNotificationAction.text(
    'reply',
    'Reply',
    buttonTitle: 'Reply',
    placeholder: 'Type your reply here',
  );

  final DarwinNotificationAction archiveAction = DarwinNotificationAction.plain(
    'archive',
    'Archive',
    options: {
      DarwinNotificationActionOption.destructive, // Example: Red text
    },
  );

  final DarwinNotificationCategory messageCategory = DarwinNotificationCategory(
    'message_category', // This identifier is used in DarwinNotificationDetails
    actions: [
      replyAction,
      archiveAction,
    ],
    options: {
      DarwinNotificationCategoryOption.hiddenPreviewShowTitle,
    },
  );

  final DarwinInitializationSettings initializationSettingsDarwin =
      DarwinInitializationSettings(
    requestAlertPermission: true,
    requestBadgePermission: true,
    requestSoundPermission: true,
    notificationCategories: [messageCategory], // Register categories
    onDidReceiveLocalNotification: (int id, String? title, String? body, String? payload) async {
      // Handle foreground notifications (iOS < 10)
    },
  );

  final InitializationSettings initializationSettings = InitializationSettings(
    android: AndroidInitializationSettings('app_icon'),
    iOS: initializationSettingsDarwin,
    macOS: initializationSettingsDarwin,
  );

  await flutterLocalNotificationsPlugin.initialize(
    initializationSettings,
    onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async {
      // Handle notification and action responses
      if (notificationResponse.notificationResponseType == NotificationResponseType.selectedNotificationAction) {
        print('iOS Action ID: ${notificationResponse.actionId}');
        if (notificationResponse.actionId == 'reply') {
          print('iOS Reply input: ${notificationResponse.input}');
        } else if (notificationResponse.actionId == 'archive') {
          print('iOS Archive action triggered');
        }
      }
    },
  );
}

// To show a notification with these actions
Future showNotificationWithActionsIos() async {
  final DarwinNotificationDetails darwinNotificationDetails =
      DarwinNotificationDetails(
    categoryIdentifier: 'message_category', // Use the category identifier defined above
    presentAlert: true,
    presentBadge: true,
    presentSound: true,
  );

  await flutterLocalNotificationsPlugin.show(
    7,
    'Meeting Reminder',
    'Your meeting with Alex starts in 10 minutes.',
    NotificationDetails(iOS: darwinNotificationDetails, macOS: darwinNotificationDetails),
    payload: 'meeting_id_456',
  );
}

Conclusion

By leveraging the flutter_local_notifications plugin, you can go beyond basic alerts to create rich, interactive Notification Center widgets in your Flutter applications. Implementing grouping enhances clarity, while extended action buttons provide convenient shortcuts for users, ultimately leading to a more streamlined and engaging mobile experience.

Remember to test your notification implementations thoroughly on both Android and iOS devices to ensure consistent behavior and a polished user interface.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is