Notification Dropdown
The NotificationDropdown bell: an unread badge over a notification stream, a popover panel listing the rows, its callbacks and how to restyle it.
On this page
Mounting the Bell#
import 'package:flutter/widgets.dart' hide Notification;
import 'package:magic/magic.dart';
import 'package:magic_notifications/magic_notifications.dart';
Widget notificationBell() {
return NotificationDropdown(
notificationStream: Notify.notifications(),
onMarkAsRead: (String id) => Notify.markAsRead(id),
onMarkAllAsRead: () => Notify.markAllAsRead(),
onNotificationTap: (DatabaseNotification notification) {
final String? url = notification.actionUrl;
if (url != null) MagicRoute.to(url);
},
// `/notifications` is the host's own route to the inbox screen.
onViewAll: () => MagicRoute.to('/notifications'),
);
}
The widget holds no state of its own and calls no backend: it renders whatever notificationStream emits and hands every action to a callback. Notify.notifications() emits the manager's held list at once and every later change, so the bell fills once something reads the inbox (Notify.fetchNotifications(), Notify.startPolling() or Notify.startRealtime(channel: ...); realtime answers false and reads nothing without a channel or a broadcast driver, so keep polling then), and Notify.markAsRead changes the held list before the request, so the badge drops on the tap.
Constructor Parameters#
| Parameter | Type | Default | Meaning |
|---|---|---|---|
notificationStream |
Stream<List<DatabaseNotification>> |
required | The rows to list and count. The unread count is the rows whose isRead is false. |
onMarkAsRead |
Future<void> Function(String id)? |
null |
Awaited on every row tap, before onNotificationTap. |
onMarkAllAsRead |
Future<void> Function()? |
null |
The heading's mark-all link; without it the link is not drawn. |
onNotificationTap |
void Function(DatabaseNotification)? |
null |
Called after onMarkAsRead; the panel closes afterwards. |
onViewAll |
VoidCallback? |
null |
The footer link; without it the footer is not drawn. |
panelClassName |
String |
kNotificationDropdownPanelClassName |
The popover panel, width (w-80) included. |
triggerClassName |
String |
kNotificationDropdownTriggerClassName |
The box around the bell, its hover: and active: tones included. |
triggerIconClassName |
String |
kNotificationDropdownTriggerIconClassName |
The bell glyph; its size (text-2xl) lives here. |
triggerHitClassName |
String? |
null |
An invisible tap box around the trigger, for a trigger drawn smaller than a finger needs. |
panelOffset |
Offset |
Offset(0, 4) |
Where the panel sits relative to the trigger's bottom-right corner. |
badgeClassName |
String |
kNotificationDropdownBadgeClassName |
The unread pill. |
badgeTextClassName |
String |
kNotificationDropdownBadgeTextClassName |
The count inside the pill. |
What the Panel Shows#
| Stream state | Bell | Panel |
|---|---|---|
| Waiting for its first list | No badge | A progress indicator |
| An error | No badge | notifications.load_failed |
| An empty list | No badge | notifications.empty |
| Rows | The unread count, or notifications.badge_overflow above nine |
notifications.title, the mark-all link while something is unread, one row per notification (title, body, relative time) and the view-all footer |
The panel is capped at 400 logical pixels high and scrolls inside. Every string comes from a translation key the host supplies; Translation Keys lists them with English reference copy.
Restyling#
The defaults are Wind's own palette (bg-white, text-gray-500, bg-red-500). Each className parameter replaces its default outright rather than appending to it, so an override restates the layout tokens and the dark: pair it still wants. The defaults are exported as kNotificationDropdown*ClassName constants to read from.
import 'package:flutter/widgets.dart' hide Notification;
import 'package:magic_notifications/magic_notifications.dart';
Widget compactBell() {
return NotificationDropdown(
notificationStream: Notify.notifications(),
triggerClassName:
'w-9 h-9 rounded-lg flex items-center justify-center '
'bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800',
triggerIconClassName: 'text-[18px] text-gray-500 dark:text-gray-400',
triggerHitClassName: 'w-11 h-11 flex items-center justify-center',
panelOffset: const Offset(-4, 0),
);
}
A 36 pixel trigger centred in a 44 pixel tap box: the panel anchors to the whole box, so panelOffset pulls it back by the box's inset to line it up with the drawn trigger. The badge count is clamped to a maximum text scale whatever badgeClassName says, so a taller pill scales less than it could but never clips.
Type Icons#
Each row leads with the icon registered for its type in the notifications.icon slot family, then with the default slot, then with a neutral bell. The inbox screen reads the same slots.
import 'package:flutter/widgets.dart' hide Notification;
import 'package:magic/magic.dart';
import 'package:magic_notifications/magic_notifications.dart';
// The host adds material_ui to its own pubspec for `Icons`.
import 'package:material_ui/material_ui.dart' show Icons;
void registerTypeIcons() {
Notify.view.slot(
NotificationViewRegistry.typeIconSlotView,
'monitor_down',
(BuildContext context) {
return WIcon(
Icons.error_outline,
className: 'text-lg text-red-500 dark:text-red-400',
);
},
);
}
Gotchas#
| Mistake | Fix |
|---|---|
| A bell that stays empty | The stream only carries what something read. Call Notify.fetchNotifications(), or start polling or realtime from the authenticated layout. Notify.startRealtime() needs a channel: and answers false without one, so check the result and keep polling. |
Overriding panelClassName with a colour only |
The override replaces the default, width and dark: pair included. Restate w-80 and the dark tone. |
Expecting a tap on a read row to skip onMarkAsRead |
It runs on every tap. Notify.markAsRead on a read row posts the request again; guard it in the callback if that matters. |
| No mark-all link or footer | Both appear only when their callback is set, and the mark-all link only while something is unread. |
Raw keys such as notifications.view_all in the panel |
Add every key in Translation Keys to every locale. |
Related Documentation#
- Preferences: the preferences screen,
PushPromptand the translation keys. - Notification Manager: the held list behind
Notify.notifications()and the optimistic read-state changes. - Configuration:
polling_interval, which sets how often a polling bell refreshes.