image

12 Mar 2026

9K

35K

Building Notification Center Widgets with Grouping, Swipe, and Action Buttons in Flutter

Effective user engagement often hinges on timely and informative notifications. While basic notifications serve their purpose, modern applications demand richer, more interactive experiences within the notification center. This article will guide you through creating sophisticated notification widgets in Flutter, incorporating features like notification grouping, interactive action buttons, and understanding native swipe behaviors.

Getting Started: Flutter Local Notifications

We'll be using the popular flutter_local_notifications plugin, which provides a cross-platform API for displaying local notifications.

Installation

Add the following to your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  flutter_local_notifications: ^17.0.0
  timezone: ^0.9.0 # Required for scheduled notifications, good practice to include

Run flutter pub get.

Initialization

Before showing any notifications, the plugin must be initialized. This involves setting up platform-specific details and defining callbacks for when notifications are interacted with.


import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/material.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future initializeNotifications() async {
  // Android initialization
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('app_icon'); // 'app_icon' refers to a drawable resource name

  // Darwin (iOS/macOS) initialization
  final DarwinInitializationSettings initializationSettingsDarwin =
      DarwinInitializationSettings(
    onDidReceiveLocalNotification: (int id, String? title, String? body, String? payload) async {
      // Handle notification when app is in foreground on iOS
      debugPrint('Foreground iOS Notification: $id, $title, $body, $payload');
    },
    requestAlertPermission: true,
    requestBadgePermission: true,
    requestSoundPermission: true,
  );

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

  await flutterLocalNotificationsPlugin.initialize(
    initializationSettings,
    onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
    onDidReceiveBackgroundNotificationResponse: onDidReceiveBackgroundNotificationResponse,
  );
}

// Handles notification responses when the app is in the foreground, background, or terminated.
void onDidReceiveNotificationResponse(NotificationResponse notificationResponse) async {
  final String? payload = notificationResponse.payload;
  final String? actionId = notificationResponse.actionId;
  final String? input = notificationResponse.input;

  debugPrint('Notification tapped!');
  debugPrint('Payload: $payload');
  debugPrint('Action ID: $actionId');
  debugPrint('Input: $input');

  if (notificationResponse.notificationResponseType == NotificationResponseType.selectedNotification) {
    // User tapped the notification itself
    if (payload != null) {
      debugPrint('Notification payload: $payload');
      // Navigate to a specific screen or perform an action based on payload
    }
  } else if (notificationResponse.notificationResponseType == NotificationResponseType.selectedNotificationAction) {
    // User tapped an action button
    if (actionId == 'reply_action') {
      debugPrint('Reply action tapped. Input: $input');
      // Process the reply input
    } else if (actionId == 'mark_read_action') {
      debugPrint('Mark as Read action tapped.');
      // Mark the message as read
    }
    // Handle other actions
  }
}

// Handles notification responses specifically when the app is in the background or terminated,
// for action buttons.
@pragma('vm:entry-point')
void onDidReceiveBackgroundNotificationResponse(NotificationResponse notificationResponse) {
  final String? payload = notificationResponse.payload;
  final String? actionId = notificationResponse.actionId;
  final String? input = notificationResponse.input;

  debugPrint('Background notification response!');
  debugPrint('Payload: $payload');
  debugPrint('Action ID: $actionId');
  debugPrint('Input: $input');

  if (notificationResponse.notificationResponseType == NotificationResponseType.selectedNotificationAction) {
    // Handle action button tap in background
    if (actionId == 'reply_action') {
      debugPrint('Background: Reply action tapped. Input: $input');
      // Perform background processing or launch the app to a specific screen
    } else if (actionId == 'mark_read_action') {
      debugPrint('Background: Mark as Read action tapped.');
      // Perform background processing
    }
  }
}

// Call this in your main function or app's initState
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeNotifications();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Notification Demo')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () => showBasicNotification(),
                child: const Text('Show Basic Notification'),
              ),
              ElevatedButton(
                onPressed: () => showGroupedNotifications(),
                child: const Text('Show Grouped Notifications'),
              ),
              ElevatedButton(
                onPressed: () => showNotificationWithActions(),
                child: const Text('Show Notification with Actions'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Displaying a Basic Notification

A simple notification requires an ID, title, body, and platform-specific details.


Future showBasicNotification() async {
  const AndroidNotificationDetails androidPlatformChannelSpecifics =
      AndroidNotificationDetails(
    'basic_channel_id',
    'Basic Notifications',
    channelDescription: 'Channel for basic notifications',
    importance: Importance.defaultImportance,
    priority: Priority.defaultPriority,
  );
  const DarwinNotificationDetails darwinPlatformChannelSpecifics =
      DarwinNotificationDetails(); // For iOS/macOS

  const NotificationDetails platformChannelSpecifics = NotificationDetails(
    android: androidPlatformChannelSpecifics,
    darwin: darwinPlatformChannelSpecifics,
  );

  await flutterLocalNotificationsPlugin.show(
    0, // Notification ID
    'Hello World!',
    'This is a simple notification from Flutter.',
    platformChannelSpecifics,
    payload: 'item x',
  );
}

Grouping Notifications for a Clutter-Free Experience

Notification grouping, primarily an Android feature, allows you to bundle related notifications together into a single summary notification, keeping the notification center tidy. iOS groups notifications based on the app by default, but you can influence it with threadIdentifier.

To implement grouping on Android, you need:

  • A unique groupKey for all notifications in the group.
  • A groupChannelId (optional, but good practice if the group requires specific channel settings).
  • A summary notification with setAsGroupSummary: true.
  • Child notifications with setAsGroupSummary: false and the same groupKey.

final String groupKey = 'com.example.notifications.GROUP_KEY';
final String groupChannelId = 'grouped_channel_id';
final String groupChannelName = 'Grouped Notifications';
final String groupChannelDescription = 'Channel for grouped notifications';

Future showGroupedNotifications() async {
  // 1. Create a channel for grouped notifications (Android)
  const AndroidNotificationChannel androidGroupChannel = AndroidNotificationChannel(
    'grouped_channel',
    'Grouped Notifications',
    description: 'Notifications belonging to a group',
    importance: Importance.max,
  );
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation()
      ?.createNotificationChannel(androidGroupChannel);

  // 2. Define Android details for child notifications
  final AndroidNotificationDetails androidPlatformChannelSpecifics =
      AndroidNotificationDetails(
    'grouped_channel',
    'Grouped Notifications',
    channelDescription: 'Channel for grouped notifications',
    groupKey: groupKey,
    setAsGroupSummary: false,
    importance: Importance.max,
    priority: Priority.max,
  );

  // 3. Show child notifications
  await flutterLocalNotificationsPlugin.show(
    1, // Unique ID for first child
    'Alice',
    'Hey, how are you?',
    NotificationDetails(android: androidPlatformChannelSpecifics),
    payload: 'alice_message',
  );

  await Future.delayed(const Duration(milliseconds: 100)); // Small delay for separate display

  await flutterLocalNotificationsPlugin.show(
    2, // Unique ID for second child
    'Bob',
    'Flutter widgets are amazing!',
    NotificationDetails(android: androidPlatformChannelSpecifics),
    payload: 'bob_message',
  );

  await Future.delayed(const Duration(milliseconds: 100));

  await flutterLocalNotificationsPlugin.show(
    3, // Unique ID for third child
    'Charlie',
    'When are we meeting?',
    NotificationDetails(android: androidPlatformChannelSpecifics),
    payload: 'charlie_message',
  );

  // 4. Define Android details for the group summary notification
  final AndroidNotificationDetails androidGroupSummaryPlatformChannelSpecifics =
      AndroidNotificationDetails(
    'grouped_channel',
    'Grouped Notifications',
    channelDescription: 'Channel for grouped notifications',
    groupKey: groupKey,
    setAsGroupSummary: true, // This is the summary notification
    groupAlertBehavior: GroupAlertBehavior.children, // Only alert for children
    importance: Importance.max,
    priority: Priority.max,
    styleInformation: InboxStyleInformation(
      [
        'Alice: Hey, how are you?',
        'Bob: Flutter widgets are amazing!',
        'Charlie: When are we meeting?',
      ],
      contentTitle: '3 new messages',
      summaryText: 'You have 3 new messages',
    ),
  );

  // For iOS, grouping is mostly handled by the OS based on the app,
  // but `threadIdentifier` can be used to influence it.
  const DarwinNotificationDetails darwinPlatformChannelSpecifics =
      DarwinNotificationDetails(threadIdentifier: 'message_thread');

  // 5. Show the group summary notification
  await flutterLocalNotificationsPlugin.show(
    0, // Use 0 or another distinct ID for the summary
    '3 New Messages',
    'You have 3 new messages from various contacts.',
    NotificationDetails(
      android: androidGroupSummaryPlatformChannelSpecifics,
      darwin: darwinPlatformChannelSpecifics,
    ),
    payload: 'grouped_messages_summary',
  );
}

Adding Interactivity: Action Buttons

Action buttons allow users to interact with a notification directly from the notification center without opening the app. This significantly enhances user experience for common actions like "Reply," "Archive," or "Mark as Read."

For Android, you use AndroidNotificationAction. For iOS/macOS, you use DarwinNotificationAction and a DarwinNotificationCategory.


Future showNotificationWithActions() async {
  // Android Action Buttons
  final List androidActions = [
    AndroidNotificationAction(
      'reply_action',
      'Reply',
      titleColor: Colors.deepPurple,
      showsUserInterface: true, // Allows showing a text input box
      // For showing a direct reply input field
      inputs: [
        const AndroidNotificationActionInput(
          label: 'Type your reply...',
        ),
      ],
    ),
    AndroidNotificationAction(
      'mark_read_action',
      'Mark as Read',
      showsUserInterface: false, // Does not show a UI for this action
    ),
    AndroidNotificationAction(
      'archive_action',
      'Archive',
      cancelNotification: true, // Dismisses the notification after action
    ),
  ];

  // Darwin (iOS/macOS) Action Categories and Buttons
  final DarwinNotificationAction replyAction = DarwinNotificationAction.text(
    'reply_action',
    'Reply',
    buttonTitle: 'Reply',
    placeholder: 'Type your reply...',
  );
  final DarwinNotificationAction markReadAction = DarwinNotificationAction.plain(
    'mark_read_action',
    'Mark as Read',
  );
  final DarwinNotificationAction archiveAction = DarwinNotificationAction.plain(
    'archive_action',
    'Archive',
    options: {
      DarwinNotificationActionOption.destructive, // Makes the button red
      DarwinNotificationActionOption.foreground,
    },
  );

  final List darwinCategories = [
    DarwinNotificationCategory(
      'message_category', // This ID links the notification to these actions
      actions: [
        replyAction,
        markReadAction,
        archiveAction,
      ],
      options: {
        DarwinNotificationCategoryOption.hiddenPreviewShowTitle,
      },
    )
  ];

  // Register Darwin notification categories
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          IOSFlutterLocalNotificationsPlugin>()
      ?.setNotificationCategories(darwinCategories);
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          MacOSFlutterLocalNotificationsPlugin>()
      ?.setNotificationCategories(darwinCategories);

  // Notification Details with Actions
  final AndroidNotificationDetails androidPlatformChannelSpecifics =
      AndroidNotificationDetails(
    'action_channel_id',
    'Action Notifications',
    channelDescription: 'Channel for notifications with actions',
    importance: Importance.max,
    priority: Priority.max,
    actions: androidActions,
    // styleInformation: BigTextStyleInformation('Long text for big style.'),
  );

  final DarwinNotificationDetails darwinPlatformChannelSpecifics =
      DarwinNotificationDetails(
    categoryIdentifier: 'message_category', // Link to the defined category
  );

  final NotificationDetails platformChannelSpecifics = NotificationDetails(
    android: androidPlatformChannelSpecifics,
    darwin: darwinPlatformChannelSpecifics,
  );

  await flutterLocalNotificationsPlugin.show(
    4, // Unique ID
    'New Message from Alex',
    'Hey, are you free for a quick call later today?',
    platformChannelSpecifics,
    payload: 'alex_message_payload',
  );
}

Understanding Swipe Behavior and Dismissal

The "swipe" gesture for notifications is primarily an operating system-level interaction. Users typically swipe a notification to:

  • Dismiss it: Removing the notification from the center. This is a native OS behavior.
  • Reveal action buttons: On Android, a short swipe can reveal action buttons. On iOS, a longer swipe to the left typically reveals "Manage" and "Clear" options, and sometimes custom actions.

Flutter doesn't directly control the *swipe gesture* within the OS notification center. Instead, we define the *actions* that the OS might expose when a user interacts (e.g., swipes, long-presses) with a notification. When a notification is dismissed, you can capture this indirectly if your app tracks pending notifications and notices one is gone, but the flutter_local_notifications plugin primarily focuses on responses when a notification or its action is *tapped*.

The onDidReceiveNotificationResponse callback will fire when a notification itself is tapped, or when an action button is tapped. This is where you implement logic to respond to user choices, whether that's navigating to a specific part of your app, updating data, or replying to a message.

Conclusion

By leveraging the flutter_local_notifications plugin, you can build rich and interactive notification experiences in your Flutter applications. Implementing grouping keeps the notification center organized, while action buttons provide immediate utility, significantly enhancing user engagement. Remember to carefully consider the user experience for both Android and iOS platforms to deliver a seamless and intuitive notification system.

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