Channels
How a notification reaches its channels: the NotificationChannel contract, the database and push channels the package registers, and how to add your own.
On this page
Channel Concept#
A channel is a delivery mechanism. NotificationManager.send asks the notification which channels it wants through Notification.via(), then dispatches to each registered channel in turn.
import 'package:magic_notifications/magic_notifications.dart';
class MonitorDownNotification extends Notification {
@override
List<String> via(Notifiable notifiable) => ['database', 'push'];
}
Future<void> alert(Notifiable user) async {
await Notify.send(user, MonitorDownNotification());
}
Channels are identified by a string name. For each name the manager:
- skips it with a logged warning when no channel is registered under it,
- skips it when
isAvailableisfalse, - awaits
channel.send(notifiable, notification).
Channels run one after another. A channel that throws does not stop the rest: the manager finishes the list, logs every later failure, and rethrows the first error once all channels have been tried.
Declaring a channel in via() is therefore safe without knowing at compile time whether the device supports it.
NotificationChannel Interface#
import 'package:magic_notifications/magic_notifications.dart';
abstract class NotificationChannel {
String get name;
bool get isAvailable;
Future<void> send(Notifiable notifiable, Notification notification);
}
| Member | Contract |
|---|---|
name |
The key used in via() and by registerChannel |
isAvailable |
A synchronous guard checked before every send; false skips the channel silently |
send |
The delivery logic; receives the target entity and the notification |
DatabaseChannel#
NotificationServiceProvider.boot() registers DatabaseChannel whether or not push is configured. It is always available, and its send does nothing:
import 'package:magic_notifications/magic_notifications.dart';
class DatabaseChannel extends NotificationChannel {
@override
String get name => 'database';
@override
bool get isAvailable => true;
@override
Future<void> send(Notifiable notifiable, Notification notification) async {
// Returns without doing anything: the backend owns the rows.
}
}
How It Works#
The database channel is asymmetric:
- The backend creates: the Laravel backend calls
$user->notify(new SomeNotification()), which persists a row in thenotificationstable. - The client reads:
NotificationPollercallsNotificationManager.fetchNotifications()every 30 seconds by default (notifications.database.polling_interval), which hitsGET /notifications. With a socket,Notify.startRealtime()applies each pushed row instead. - A stream emits: results go to the broadcast stream behind
Notify.notifications(). - The UI reacts: any widget listening to that stream receives the updated list.
DatabaseChannel.send exists for parity with Laravel's via() vocabulary. A client-created row is not supported by this channel; post to your backend's own endpoint for that.
DatabaseNotification fields:
| Field | Type | Description |
|---|---|---|
id |
String |
Id from the backend |
type |
String |
Notification type (for example monitor_down) |
title |
String |
Read from data['title'] |
body |
String |
Read from data['body'] |
data |
Map<String, dynamic> |
The full data payload |
actionUrl |
String? |
Read from data['action_url'] |
createdAt |
DateTime |
Creation timestamp |
readAt |
DateTime? |
null while unread |
isRead |
bool |
readAt != null |
PushChannel#
PushChannel is registered when a push driver resolves. It is self-addressed and ships switched off.
| Property | Behaviour |
|---|---|
name |
'push' |
isAvailable |
driver.isSupported and notifications.push.self_test_enabled is true. With the switch off the manager skips the channel, so the other channels in via() still deliver. |
| Recipient | None. It posts to POST /notifications/push-test and the backend derives the recipient from the session, so the only person it can page is the authenticated one. A Notifiable whose notifiableId is not the signed-in user's id throws NotificationException with code PUSH_RECIPIENT_NOT_AUTHENTICATED_USER. |
| Payload | title (heading), body (content) and data from toPush(); a url set on the PushMessage is folded into data['url'] unless data already has one. |
| Failure | A non-successful response throws NotificationException with code HTTP_<status>. |
Ordinary push to other people is sent by your backend through OneSignal; see Laravel Backend Setup. This channel exists so a person can find out whether push reaches their own phone before an incident rather than during one. The backend carries the matching magic-starter.onesignal.self_test_enabled switch; enable both or neither.
How It Works#
- With the switch off,
sendlogs a debug line and returns. - A foreign recipient is refused with the exception above.
toPush()is called on the notification; anullreturn exits early.- When the
Notifiablehas anotificationPreference,isEnabled(notification.type, 'push')must betrue(see Preferences). - The request is sent.
Creating Custom Channels#
Extend NotificationChannel for any other delivery mechanism:
import 'package:magic/magic.dart';
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 name it in via():
import 'package:magic_notifications/magic_notifications.dart';
class DeployFinishedNotification extends Notification {
@override
List<String> via(Notifiable notifiable) => ['database', 'slack'];
// `SlackChannel.send` reads this: the base `toDatabase` returns `null`.
@override
Map<String, dynamic>? toDatabase(Notifiable notifiable) => {
'title': 'Deploy finished',
'body': 'The release is live.',
};
}
Channel Registration#
NotificationServiceProvider registers database and push itself. Register any other channel from the boot() of a provider of your own:
import 'package:magic/magic.dart';
import 'package:magic_notifications/magic_notifications.dart';
// The host's own channel from the section above, and its webhook url.
const String webhookUrl = 'https://hooks.example.com/slack';
class SlackServiceProvider extends ServiceProvider {
SlackServiceProvider(super.app);
@override
void register() {}
@override
Future<void> boot() async {
NotificationManager().registerChannel(SlackChannel(webhookUrl));
}
}
Registering a name that already exists replaces the previous instance, which is how a test swaps in a fake. hasChannel answers whether a name is registered:
import 'package:magic_notifications/magic_notifications.dart';
bool hasPushChannel() {
final bool exists = NotificationManager().hasChannel('push');
return exists;
}
Gotchas#
| Mistake | Fix |
|---|---|
Expecting DatabaseChannel.send to store a row |
It does nothing. The backend creates database notifications; the client reads them with Notify.notifications() and polling or realtime. |
Listing 'push' in via() and seeing nothing happen |
PushChannel is unavailable until notifications.push.self_test_enabled is true and the driver is supported. It only reaches the signed-in user. Send push to other people from your backend. |
Listing 'mail' in via() |
The package has no mail channel. The manager logs Unknown notification channel: mail and skips it; send mail from the backend. |
| Assuming one failing channel blocks the next | It does not. Every channel is tried, then the first error is rethrown from send; wrap the call if a failure must not surface. |
Notification is ambiguous after importing Flutter |
Flutter's widgets library also exports Notification. Import it with hide Notification (import 'package:flutter/widgets.dart' hide Notification;). |
Related Documentation#
- Drivers: the push SDK layer behind
PushChannel. - Preferences: the per-type and per-channel opt-outs
PushChannelhonours. - Notification Manager: dispatch, polling and realtime.
- Laravel Backend Setup: the backend that creates the rows and sends the pushes.
- Realtime channels with magic_socket: the socket layer
Notify.startRealtimesubscribes through.