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