Building a Flutter Notification Center with Grouping & Actions

02 Aug 2026

9K

35K

Building a Flutter Notification Center with Grouping & Actions

Modern mobile applications often rely on notifications to keep users informed and engaged. While system-level notifications are crucial for out-of-app alerts, an in-app notification center provides a dedicated space for users to review, manage, and interact with important updates directly within your application. This article will guide you through building a robust Flutter notification center widget, incorporating advanced features like notification grouping, swipe-to-dismiss functionality, custom icons, and interactive action buttons, ensuring a rich and intuitive user experience.

Why a Custom Notification Center in Flutter?

Relying solely on system notifications can sometimes be limiting. A custom in-app notification center offers several advantages:

  • Consistent UI/UX: Maintain your app's branding and design language, regardless of the operating system's notification style.
  • Richer Interactions: Implement complex actions, deep linking, and custom UIs that go beyond what standard system notifications typically allow.
  • Offline Access: Users can review past notifications even without an internet connection, if you persist the data locally.
  • Enhanced Control: You have full control over how notifications are displayed, grouped, and managed within your application's context.
  • Reduced Interruption: For non-critical updates, users can check the notification center at their convenience, rather than being constantly interrupted by push notifications.

Core Concepts for a Robust Notification UI

Before diving into the UI, it's essential to establish a solid foundation for your notification data and state management.

Notification Data Model

Each notification needs a structured data model to hold its information. Consider the following properties:

class AppNotification {  final String id;  final String title;  final String body;  final DateTime timestamp;  final NotificationType type;  bool isRead;  final List<NotificationAction> actions;  final String? groupId; // For grouping notifications  AppNotification({    required this.id,    required this.title,    required this.body,    required this.timestamp,    this.type = NotificationType.info,    this.isRead = false,    this.actions = const [],    this.groupId,  });  // Helper to mark as read  AppNotification copyWith({bool? isRead}) {    return AppNotification(      id: id,      title: title,      body: body,      timestamp: timestamp,      type: type,      isRead: isRead ?? this.isRead,      actions: actions,      groupId: groupId,    );  }}enum NotificationType { info, warning, success, error, promotion }class NotificationAction {  final String label;  final VoidCallback onPressed;  NotificationAction({required this.label, required this.onPressed});}

The NotificationType enum helps in displaying custom icons, while NotificationAction defines interactive buttons.

State Management for Notifications

You'll need a mechanism to manage the list of notifications (add, remove, mark as read, etc.). For simplicity, we'll use a basic ChangeNotifier, but for larger applications, consider providers like Riverpod or BLoC.

class NotificationService extends ChangeNotifier {  final List<AppNotification> _notifications = [];  List<AppNotification> get notifications => List.unmodifiable(_notifications);  void addNotification(AppNotification notification) {    _notifications.insert(0, notification); // Add to top    notifyListeners();  }  void removeNotification(String id) {    _notifications.removeWhere((n) => n.id == id);    notifyListeners();  }  void markAsRead(String id) {    final index = _notifications.indexWhere((n) => n.id == id);    if (index != -1) {      _notifications[index] = _notifications[index].copyWith(isRead: true);      notifyListeners();    }  }  // ... other methods like clearAll, markGroupAsRead, etc.}

Step-by-Step: Building the Notification Item Widget

Each notification in the center will be represented by a customizable widget.

Basic Notification Card with Custom Icons

Start with a simple card structure. We'll use a Row to place a custom icon next to the notification content.

class NotificationItem extends StatelessWidget {  final AppNotification notification;  final ValueChanged<String> onDismissed;  final VoidCallback? onTap;  const NotificationItem({    Key? key,    required this.notification,    required this.onDismissed,    this.onTap,  }) : super(key: key);  IconData _getIconForType(NotificationType type) {    switch (type) {      case NotificationType.info:        return Icons.info_outline;      case NotificationType.warning:        return Icons.warning_amber_outlined;      case NotificationType.success:        return Icons.check_circle_outline;      case NotificationType.error:        return Icons.error_outline;      case NotificationType.promotion:        return Icons.star_outline;    }  }  @override  Widget build(BuildContext context) {    return Card(      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),      color: notification.isRead ? Colors.grey[200] : Colors.white,      child: InkWell(        onTap: onTap,        child: Padding(          padding: const EdgeInsets.all(12.0),          child: Row(            crossAxisAlignment: CrossAxisAlignment.start,            children: [              Icon(                _getIconForType(notification.type),                color: notification.isRead ? Colors.grey : Theme.of(context).primaryColor,                size: 28,              ),              const SizedBox(width: 12),              Expanded(                child: Column(                  crossAxisAlignment: CrossAxisAlignment.start,                  children: [                    Text(                      notification.title,                      style: TextStyle(                        fontWeight: FontWeight.bold,                        fontSize: 16,                        color: notification.isRead ? Colors.grey[600] : Colors.black,                      ),                    ),                    const SizedBox(height: 4),                    Text(                      notification.body,                      style: TextStyle(                        fontSize: 14,                        color: notification.isRead ? Colors.grey[500] : Colors.black87,                      ),                      maxLines: 2,                      overflow: TextOverflow.ellipsis,                    ),                    const SizedBox(height: 8),                    Text(                      '${notification.timestamp.hour}:${notification.timestamp.minute} - ${notification.timestamp.day}/${notification.timestamp.month}',                      style: TextStyle(                        fontSize: 12,                        color: notification.isRead ? Colors.grey[400] : Colors.grey[600],                      ),                    ),                  ],                ),              ),            ],          ),        ),      ),    );  }}

Implementing Swipe-to-Dismiss

Flutter's Dismissible widget makes swipe-to-dismiss straightforward. Wrap your NotificationItem with it.

// ... inside NotificationCenterView's ListView.builderitemBuilder: (context, index) {  final notification = notifications[index];  return Dismissible(    key: Key(notification.id), // Unique key is crucial    direction: DismissDirection.endToStart, // Only swipe left    background: Container(      color: Colors.red,      alignment: Alignment.centerRight,      padding: const EdgeInsets.symmetric(horizontal: 20),      child: const Icon(Icons.delete, color: Colors.white),    ),    onDismissed: (direction) {      Provider.of<NotificationService>(context, listen: false)          .removeNotification(notification.id);      ScaffoldMessenger.of(context).showSnackBar(        SnackBar(content: Text('${notification.title} dismissed')),      );    },    child: NotificationItem(      notification: notification,      onDismissed: (id) {}, // Handled by Dismissible directly      onTap: () {        Provider.of<NotificationService>(context, listen: false)            .markAsRead(notification.id);        // Navigate to detail screen or perform action      },    ),  );},

Integrating Action Buttons

Action buttons can be placed within the NotificationItem, perhaps conditionally shown, or revealed on a swipe. For simplicity, let's add them at the bottom of the card if actions are present.

// ... inside NotificationItem's Column, after the timestamp Text widgetif (notification.actions.isNotEmpty) {  const SizedBox(height: 8);  Row(    mainAxisAlignment: MainAxisAlignment.end,    children: notification.actions.map((action) {      return Padding(        padding: const EdgeInsets.only(left: 8.0),        child: OutlinedButton(          onPressed: action.onPressed,          child: Text(action.label),        ),      );    }).toList(),  ),}

Grouping Notifications Effectively

Grouping notifications enhances readability, especially when a user receives many similar alerts (e.g., multiple messages from the same chat, or several updates from a single app feature).

Grouping Logic

You can group notifications by groupId, which could represent a sender, a topic, or a specific event. In your NotificationService, you'd process the raw list into a grouped structure.

// Example of grouping logic in a separate helper or NotificationServiceList<MapEntry<String?, List<AppNotification>>> getGroupedNotifications(    List<AppNotification> notifications) {  final Map<String?, List<AppNotification>> grouped = {};  for (var n in notifications) {    final key = n.groupId ?? 'Ungrouped'; // Use a default key for ungrouped    if (!grouped.containsKey(key)) {      grouped[key] = [];    }    grouped[key]!.add(n);  }  return grouped.entries.toList();}

UI for Grouped Notifications

For the UI, you can use an ExpansionTile to allow users to expand and collapse groups.

// ... inside NotificationCenterView's build methodConsumer<NotificationService>(  builder: (context, service, child) {    final groupedNotifications = getGroupedNotifications(service.notifications);    return ListView.builder(      itemCount: groupedNotifications.length,      itemBuilder: (context, groupIndex) {        final groupEntry = groupedNotifications[groupIndex];        final groupKey = groupEntry.key;        final notificationsInGroup = groupEntry.value;        if (groupKey == 'Ungrouped') {          // Render ungrouped notifications directly          return Column(            children: notificationsInGroup.map((notification) {              return Dismissible(                key: Key(notification.id),                direction: DismissDirection.endToStart,                background: _buildDismissBackground(),                onDismissed: (dir) => service.removeNotification(notification.id),                child: NotificationItem(                  notification: notification,                  onDismissed: (id) {},                  onTap: () => service.markAsRead(notification.id),                ),              );            }).toList(),          );        }        // Render grouped notifications with ExpansionTile        return ExpansionTile(          title: Text('$groupKey (${notificationsInGroup.length})'),          children: notificationsInGroup.map((notification) {            return Dismissible(              key: Key(notification.id),              direction: DismissDirection.endToStart,              background: _buildDismissBackground(),              onDismissed: (dir) => service.removeNotification(notification.id),              child: NotificationItem(                notification: notification,                onDismissed: (id) {},                onTap: () => service.markAsRead(notification.id),              ),            );          }).toList(),        );      },    );  },)// Helper for dismissible backgroundWidget _buildDismissBackground() {  return Container(    color: Colors.red,    alignment: Alignment.centerRight,    padding: const EdgeInsets.symmetric(horizontal: 20),    child: const Icon(Icons.delete, color: Colors.white),  );  }

Assembling the Notification Center View

Finally, combine all these elements into a full-fledged notification center screen. We'll use ChangeNotifierProvider to make our NotificationService accessible.

import 'package:flutter/material.dart';import 'package:provider/provider.dart';// Assume AppNotification, NotificationType, NotificationAction, NotificationService, NotificationItem are defined aboveclass NotificationCenterScreen extends StatelessWidget {  const NotificationCenterScreen({Key? key}) : super(key: key);  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: const Text('Notification Center'),        actions: [          IconButton(            icon: const Icon(Icons.mark_email_read),            tooltip: 'Mark all as read',            onPressed: () {              Provider.of<NotificationService>(context, listen: false).notifications.forEach((n) {                if (!n.isRead) {                  Provider.of<NotificationService>(context, listen: false).markAsRead(n.id);                }              });            },          ),          IconButton(            icon: const Icon(Icons.clear_all),            tooltip: 'Clear all',            onPressed: () {              // Implement clear all logic in NotificationService              // Provider.of<NotificationService>(context, listen: false).clearAll();            },          ),        ],      ),      body: Consumer<NotificationService>(        builder: (context, service, child) {          final notifications = service.notifications;          if (notifications.isEmpty) {            return const Center(              child: Text(                'No new notifications',                style: TextStyle(fontSize: 16, color: Colors.grey),              ),            );          }          final groupedNotifications = getGroupedNotifications(notifications); // Using the helper          return ListView.builder(            itemCount: groupedNotifications.length,            itemBuilder: (context, groupIndex) {              final groupEntry = groupedNotifications[groupIndex];              final groupKey = groupEntry.key;              final notificationsInGroup = groupEntry.value;              if (groupKey == 'Ungrouped') {                return Column(                  children: notificationsInGroup.map((notification) {                    return _buildDismissibleNotification(context, service, notification);                  }).toList(),                );              }              return ExpansionTile(                title: Text('$groupKey (${notificationsInGroup.length})'),                children: notificationsInGroup.map((notification) {                  return _buildDismissibleNotification(context, service, notification);                }).toList(),              );            },          );        },      ),    );  }  Widget _buildDismissibleNotification(BuildContext context, NotificationService service, AppNotification notification) {    return Dismissible(      key: Key(notification.id),      direction: DismissDirection.endToStart,      background: Container(        color: Colors.red,        alignment: Alignment.centerRight,        padding: const EdgeInsets.symmetric(horizontal: 20),        child: const Icon(Icons.delete, color: Colors.white),      ),      onDismissed: (direction) {        service.removeNotification(notification.id);        ScaffoldMessenger.of(context).showSnackBar(          SnackBar(content: Text('${notification.title} dismissed')),        );      },      child: NotificationItem(        notification: notification,        onDismissed: (id) {},        onTap: () {          service.markAsRead(notification.id);          // Handle navigation or specific action based on notification content        },      ),    );  }}

Best Practices and Considerations

  • Performance: For very large lists of notifications, ensure efficient rendering. ListView.builder is good, but consider package solutions like flutter_slidable for more advanced swipe actions if Dismissible isn't sufficient.
  • Persistence: Notifications should ideally persist across app sessions. Use local storage solutions like shared_preferences, sqflite, or Hive to save and load notifications.
  • Accessibility: Ensure your notification center is accessible to all users. Use semantic widgets, provide adequate contrast, and consider screen reader support.
  • User Experience: Provide clear visual cues for read/unread status. Ensure swipe gestures are intuitive and provide immediate feedback.
  • Notification Badges: Integrate a counter for unread notifications on your app's main navigation or icon.
  • Real-time Updates: For real-time applications, integrate with a backend service (e.g., Firebase Cloud Messaging, WebSockets) to push new notifications to the service.

Conclusion

Building a custom notification center in Flutter provides a powerful way to enhance user engagement and control the in-app experience. By implementing features like grouping, swipe actions, custom icons, and interactive buttons, you can create a highly functional and visually appealing notification system tailored to your application's needs. Start by defining your data model, manage state effectively, and then progressively build out the UI components to deliver a superior user experience.

FAQ

  1. How do I make notifications persistent across app restarts?

    You can use local storage solutions like shared_preferences for simple lists, or sqflite/Hive for more complex data. When the app starts, load the saved notifications into your NotificationService.

  2. Can I use different swipe actions (e.g., swipe left for delete, swipe right for archive)?

    Yes, Flutter's Dismissible widget supports DismissDirection.startToEnd and DismissDirection.endToStart. For more complex multi-action swipes, consider using a package like flutter_slidable, which offers more granular control over leading and trailing actions.

  3. How can I add a badge count for unread notifications?

    Your NotificationService can expose a getter like unreadCount that filters _notifications where isRead is false. Listen to this count (e.g., using Consumer or Selector) and display it on an icon or navigation item.

  4. What's the best way to handle navigation when a notification is tapped?

    The onTap callback in NotificationItem is the ideal place. You can pass a specific route or a callback function to navigate to a detail screen or trigger an action relevant to that notification.

Related Articles

Aug 02, 2026

Building a Flutter Notification Center with Grouping & Actions

Learn to create a sophisticated in-app notification center in Flutter, featuring grouping, swipe-to-dismiss, custom icons, and actionable buttons for enhanced u

Aug 01, 2026

Flutter Layout Tips: Using FractionallySizedBox, AspectRatio, ConstrainedBox for Adaptive UI

Master Flutter's FractionallySizedBox, AspectRatio, and ConstrainedBox to build truly adaptive UIs. Learn how to control widget sizing, maintain proportions, an

Aug 01, 2026

Flutter Product Quick View: Animation, Related Items, Badges

Learn to create a dynamic product quick view widget in Flutter, incorporating smooth animations, relevant related items, and eye-catching promo badges for an en