Channels
On this page
Table of Contents#
- Channel Concept
- NotificationChannel Interface
- DatabaseChannel
- PushChannel
- Creating Custom Channels
- Channel Registration
Channel Concept#
A channel is a delivery mechanism for notifications. When NotificationManager.send is called, it asks the notification which channels it wants via Notification.via(), then dispatches to each registered channel in turn.
class MonitorDownNotification extends Notification {
@override
List<String> via(Notifiable notifiable) => ['database', 'push'];
}
Channels are identified by a string name. The manager skips unknown channel names (logs a warning) and skips channels where isAvailable returns false. This makes it safe to declare channels in via() without knowing whether the device supports them at compile time.
NotificationChannel Interface#
abstract class NotificationChannel {
/// Channel identifier (e.g., 'database', 'push', 'mail')
String get name;
/// Whether this channel is available and configured
bool get isAvailable;
/// Send notification through this channel
Future<void> send(Notifiable notifiable, Notification notification);
}
Every channel must implement these three members:
name— the string key used invia()return values andregisterChannelisAvailable— a synchronous guard checked before everysendcall; returnfalseto silently skipsend— the actual delivery logic; receives the target entity and the notification
DatabaseChannel#
class DatabaseChannel extends NotificationChannel {
@override
String get name => 'database';
@override
bool get isAvailable => true;
@override
Future<void> send(Notifiable notifiable, Notification notification) async {
final data = notification.toDatabase(notifiable);
if (data == null) return;
// no-op: database notifications are created server-side
}
}
How It Works#
The database channel follows an asymmetric pattern:
- Backend creates — the Laravel backend calls
$user->notify(new SomeNotification()), which persists a row in thenotificationstable - Frontend polls —
NotificationPollercallsNotificationManager.fetchNotifications()every 30 seconds (configurable), which hitsGET /notifications - Stream emits — results are pushed to the broadcast
StreamController<List<DatabaseNotification>> - UI reacts — any widget listening to
Notify.notifications()receives the updated list
The send() method in DatabaseChannel is intentionally a no-op. It exists for API parity with Laravel's notification system. When toDatabase() returns null, send() exits immediately; otherwise, the data map is validated but not forwarded (the backend owns persistence).
DatabaseNotification model fields:
| Field | Type | Description |
|---|---|---|
id |
String |
UUID from backend |
type |
String |
Notification class name (e.g., monitor_down) |
title |
String |
Display title |
body |
String |
Display body |
data |
Map<String, dynamic> |
Full data payload |
actionUrl |
String? |
Deep-link or route path |
createdAt |
DateTime |
Creation timestamp |
readAt |
DateTime? |
null when unread |
isRead |
bool |
readAt != null |
PushChannel#
class PushChannel extends NotificationChannel {
final PushDriver _driver;
PushChannel(this._driver);
@override
String get name => 'push';
@override
bool get isAvailable => _driver.isSupported;
@override
Future<void> send(Notifiable notifiable, Notification notification) async {
final pushMessage = notification.toPush(notifiable);
if (pushMessage == null) return;
final preference = notifiable.notificationPreference;
if (preference != null) {
final pushEnabled = preference.isEnabled(notification.type, 'push');
if (!pushEnabled) return;
}
// Dispatches to backend → OneSignal
}
}
How It Works#
toPush()is called on the notification; anullreturn exits early- If the
Notifiablehas anotificationPreference,isEnabled(type, 'push')is checked against global + per-type preferences isAvailabledelegates to_driver.isSupported—falseon web forOneSignalDriver,falseon mobile forOneSignalWebDriver
Creating Custom Channels#
Extend NotificationChannel to add any delivery mechanism:
import 'package:magic_notifications/magic_notifications.dart';
class SlackChannel extends NotificationChannel {
final String _webhookUrl;
SlackChannel(this._webhookUrl);
@override
String get name => 'slack';
@override
bool get isAvailable => _webhookUrl.isNotEmpty;
@override
Future<void> send(Notifiable notifiable, Notification notification) async {
final data = notification.toDatabase(notifiable);
if (data == null) return;
await Http.post(_webhookUrl, data: {
'text': '${data['title']}: ${data['body']}',
});
}
}
Then declare 'slack' in via():
@override
List<String> via(Notifiable notifiable) => ['database', 'slack'];
Channel Registration#
Channels are registered on NotificationManager before any notifications are dispatched. NotificationServiceProvider.register() is the conventional place:
@override
void register() {
final manager = NotificationManager();
manager.registerChannel(DatabaseChannel());
manager.registerChannel(PushChannel(driver));
// manager.registerChannel(SlackChannel(webhookUrl));
app.singleton('notifications', () => manager);
}
Registration is idempotent per name — registering a channel with an existing name replaces the previous instance.
// Check registration
final bool exists = NotificationManager().hasChannel('push');
Related