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
groupKeyfor 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: falseand the samegroupKey.
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.