Building Dynamic Notification Card Widgets in Flutter
Notification cards are an integral part of modern mobile applications, providing users with timely updates, alerts, and information. Building these widgets dynamically in Flutter allows for a responsive user interface that can adapt to changing data, offering a seamless user experience. This article will guide you through the process of creating dynamic notification card widgets, covering data modeling, UI implementation, and managing state.
Understanding the Core Components
To construct a dynamic notification system, we'll break it down into three primary components:
- Notification Data Model: A structured way to represent individual notification data (e.g., title, body, timestamp, read status).
- Notification Card Widget: A reusable Flutter widget that visually displays a single notification based on the data model.
- Notification List View: A parent widget responsible for fetching, managing, and displaying a collection of notification cards dynamically.
Step-by-Step Implementation
1. Defining the Notification Data Model
First, let's create a simple data model to represent a single notification. This class will hold all the necessary information for each notification card.
// lib/models/notification_model.dart
class NotificationModel {
final String id;
final String title;
final String body;
final DateTime timestamp;
bool isRead; // To track if the notification has been read
NotificationModel({
required this.id,
required this.title,
required this.body,
required this.timestamp,
this.isRead = false, // Default to unread
});
}
This model includes basic fields like `id` (for unique identification), `title`, `body`, `timestamp`, and a boolean `isRead` property which will be crucial for dynamic styling and behavior.
2. Creating the Notification Card Widget
Next, we'll build the UI for a single notification card. This will be a StatelessWidget as it primarily focuses on displaying the data it receives. It will also include callbacks to communicate user interactions (like dismissing or tapping) back to its parent.
// lib/widgets/notification_card.dart
import 'package:flutter/material.dart';
import '../models/notification_model.dart';
class NotificationCard extends StatelessWidget {
final NotificationModel notification;
final ValueChanged onDismissed; // Callback for dismissing
final ValueChanged onTapped; // Callback for tapping (e.g., mark as read)
const NotificationCard({
Key? key,
required this.notification,
required this.onDismissed,
required this.onTapped,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
elevation: 2,
// Change background color based on read status
color: notification.isRead ? Colors.grey[200] : Colors.white,
child: InkWell(
onTap: () => onTapped(notification.id), // Invoke callback on tap
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icon changes based on read status
Icon(
notification.isRead ? Icons.notifications_off : Icons.notifications_active,
color: notification.isRead ? Colors.grey : Theme.of(context).primaryColor,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
notification.title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: notification.isRead ? Colors.grey[600] : Colors.black87,
),
),
const SizedBox(height: 4),
Text(
notification.body,
style: TextStyle(
fontSize: 14,
color: notification.isRead ? Colors.grey[500] : Colors.black54,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
'${notification.timestamp.day}/${notification.timestamp.month} ${notification.timestamp.hour}:${notification.timestamp.minute}',
style: TextStyle(
fontSize: 12,
color: notification.isRead ? Colors.grey[400] : Colors.black38,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () => onDismissed(notification.id), // Invoke callback on dismiss
color: Colors.grey,
),
],
),
),
),
);
}
}
This NotificationCard widget displays the notification's details, changes its appearance based on the isRead status, and provides an "X" icon to dismiss it. The onTapped and onDismissed callbacks are essential for interacting with the parent widget.
3. Building the Notification List View
The main logic for managing notifications (adding, removing, marking as read) will reside in a StatefulWidget that displays a list of NotificationCard widgets. We'll use ListView.builder for efficient rendering of potentially large lists.
// lib/pages/notification_list_page.dart
import 'package:flutter/material.dart';
import '../models/notification_model.dart';
import '../widgets/notification_card.dart';
class NotificationListPage extends StatefulWidget {
const NotificationListPage({Key? key}) : super(key: key);
@override
State createState() => _NotificationListPageState();
}
class _NotificationListPageState extends State {
// Dummy data for demonstration. In a real app, this would come from an API or database.
List _notifications = [
NotificationModel(
id: '1',
title: 'New Message from Admin',
body: 'You have received a new message regarding your recent activity.',
timestamp: DateTime.now().subtract(const Duration(minutes: 5)),
isRead: false,
),
NotificationModel(
id: '2',
title: 'Reminder: Upcoming Event',
body: 'Your meeting with John Doe is scheduled for tomorrow at 10 AM.',
timestamp: DateTime.now().subtract(const Duration(hours: 2)),
isRead: false,
),
NotificationModel(
id: '3',
title: 'Payment Confirmed',
body: 'Your payment of \$50 has been successfully processed.',
timestamp: DateTime.now().subtract(const Duration(days: 1)),
isRead: true, // Already read
),
NotificationModel(
id: '4',
title: 'Special Offer Just For You!',
body: 'Don\'t miss out on our limited-time discounts for premium members.',
timestamp: DateTime.now().subtract(const Duration(days: 3)),
isRead: false,
),
];
// Method to mark a specific notification as read
void _markAsRead(String notificationId) {
setState(() {
final index = _notifications.indexWhere((notif) => notif.id == notificationId);
if (index != -1) {
_notifications[index].isRead = true;
}
});
}
// Method to remove a notification from the list
void _dismissNotification(String notificationId) {
setState(() {
_notifications.removeWhere((notif) => notif.id == notificationId);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Notifications'),
actions: [
IconButton(
icon: const Icon(Icons.mark_email_read),
onPressed: () {
setState(() {
// Mark all notifications as read
for (var notif in _notifications) {
notif.isRead = true;
}
});
},
tooltip: 'Mark all as read',
)
],
),
body: _notifications.isEmpty
? Center(
child: Text(
'No new notifications',
style: Theme.of(context).textTheme.headlineSmall,
),
)
: ListView.builder(
itemCount: _notifications.length,
itemBuilder: (context, index) {
final notification = _notifications[index];
return NotificationCard(
// Use ValueKey for efficient list updates when items are added/removed
key: ValueKey(notification.id),
notification: notification,
onDismissed: _dismissNotification,
onTapped: _markAsRead,
);
},
),
);
}
}
And finally, a simple `main.dart` to run your application:
// lib/main.dart
import 'package:flutter/material.dart';
import 'pages/notification_list_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Dynamic Notifications Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const NotificationListPage(),
);
}
}
In the _NotificationListPageState, we define a list of NotificationModel objects. The _markAsRead and _dismissNotification methods update this list and call setState() to rebuild the UI, reflecting the changes dynamically. The ListView.builder creates a NotificationCard for each item in the list, passing the respective notification data and registering the callback functions.
4. Adding Dynamic Behavior (Dismissing and Marking as Read)
The dynamism comes from how the parent widget (NotificationListPage) reacts to the callbacks from the child widgets (NotificationCard). When a user taps an "X" on a card, onDismissed is called, which triggers _dismissNotification in the parent. This removes the notification from the _notifications list and rebuilds the ListView, making the card disappear.
Similarly, tapping anywhere on the card invokes onTapped, which calls _markAsRead. This updates the isRead status of the corresponding notification model and triggers a UI rebuild, changing the card's background and icon dynamically.
Styling and Enhancements
- Animations: Consider using
AnimatedListorDismissiblewidgets for more fluid animations when adding or removing cards. - Custom Icons and Avatars: Enhance visual appeal by adding user profile pictures or specific icons related to the notification type.
- Swipe-to-Dismiss: Implement
Dismissiblewidget directly around theNotificationCardfor a common mobile UX pattern. - Pagination/Lazy Loading: For a very large number of notifications, implement lazy loading to fetch data in chunks as the user scrolls.
- Backend Integration: Replace dummy data with actual data fetched from a backend API, potentially using a state management solution like Provider, BLoC, or Riverpod for robust data flow.
- Grouping Notifications: Implement logic to group similar notifications or notifications from the same sender together.
Conclusion
Building dynamic notification card widgets in Flutter involves a clear separation of concerns: a robust data model, a reusable UI component, and a stateful parent widget to manage the collection. By leveraging StatefulWidget, ListView.builder, and callbacks, you can create a highly interactive and responsive notification system that keeps users informed and engaged. This foundation can be further extended with animations, backend integration, and advanced UI/UX patterns to deliver a sophisticated notification experience.