Tutorial · 20 min · advanced
Notifications, local & push
Permission, local alerts, chat-style banners with avatars on both platforms, and the FCM token pipeline — firebase_messaging's job, over FFI.
What you'll build — running on device.
Notifications span two plugins that work together:
dartnative_notifications for everything local — permission, alerts, the
chat-style banner (iOS Communication Notifications / Android
MessagingStyle-like avatars) — and dartnative_firebase for FCM push,
on BOTH platforms. You’ll wire the full pipeline: ask, show, style, then fetch a real
push token and watch foreground pushes stream in. It’s the DartNative
playground’s notifications demo, carried into the tutorial byte-for-byte.
The local half needs zero configuration; push needs a
Firebase project (the README lists the steps).
What you need
- A project from Your first DartNative app
dartnative_notifications: ^1.0.0anddartnative_firebase: ^1.0.0- For the FCM sections: your
GoogleService-Info.plist, the Push Notifications + Background Modes → Remote notifications capabilities, and a physical device (APNs skips simulators)
Step 0 — Push configuration
The local half runs with zero setup — skip ahead if that’s all you need. For FCM push, four one-time steps:
- Set a bundle identifier unique to your team (push needs an explicit
App ID — the scaffold’s
com.example.*placeholder is already claimed and fails team registration), register an iOS app with that ID in your Firebase project, and drop itsGoogleService-Info.plistintoios/Runner/— the Runner target already references it there. - Add the Push Notifications capability to the Runner target (Xcode:
Signing & Capabilities → + Capability). By hand that’s a
Runner.entitlementswithaps-environment=development, wired via theCODE_SIGN_ENTITLEMENTSbuild setting. Miss this and the token request times out — APNs registration silently never completes. - For background delivery, also tick Background Modes → Remote notifications.
- Upload an APNs auth key in the Firebase console (Project settings → Cloud Messaging).
On Android, drop google-services.json into android/app/ and apply the
Google Services Gradle plugin; on Android 13+ the
POST_NOTIFICATIONS permission must be in the manifest and granted at
runtime (the demo’s permission button requests it). Also ship a
notification_icon.png (white silhouette on transparency, five
densities) — Android tints status-bar icons by alpha only, so the
full-colour launcher fallback renders as a solid black square; the
plugin README has the sizes and a generator script.
Step 1 — Set up in main()
Both plugins install a native callback surface — the object the OS
calls when a token arrives or a banner is tapped (a delegate on iOS, the
activity-event hub on Android). Callbacks want to exist before anything
can fire them, so setup lives in main(), before the first frame — the
SAME code on both platforms:
Future<void> main() async {
DartNativePluginRegistrant.registerAll();
// Firebase: loads its symbols + inits the default app from the platform
// config (GoogleService-Info.plist / google-services.json), then
// installs the FCM delegate — done in main() so the token is ready
// before any screen asks for it.
try {
await Firebase.initializeApp();
FirebaseMessaging.setup();
} catch (e) {
print('Firebase init failed — continuing without push: $e');
}
try {
DartNativeNotifications.setup(onTap: (payload) {
// A tapped notification lands here — route on the payload.
print('notification tapped: $payload');
});
} catch (e) {
print('Notifications init failed: $e');
}
runApp(const NotificationsDemo());
}
Firebase.initializeApp() reads GoogleService-Info.plist on iOS and
the google-services.json-derived config on Android;
FirebaseMessaging.setup() installs the FCM delegate. Because both
happen before runApp, the token is ready before any screen asks, and a
notification tapped from a cold start still reaches your onTap on
either platform. Each init gets its own try/catch deliberately — a
missing config file should cost you push, not the whole app.
Step 2 — Permission
Nothing shows until the user says yes, so this is the first button on the screen:
// dartnative_notifications handles permission on both platforms:
// iOS — UNUserNotificationCenter.requestAuthorization (also covers APNs/FCM)
// Android — POST_NOTIFICATIONS runtime permission (API 33+)
final granted = await DartNativeNotifications.requestPermission();
setState(() => _permissionGranted = granted);
One call, both platforms. On iOS the same authorization covers local
alerts and remote APNs/FCM pushes — you don’t ask twice. On Android
13+ it maps to the POST_NOTIFICATIONS runtime permission; on older
Android there is no runtime permission, so it reports the current
notifications-enabled state without showing a dialog.
Step 3 — A local notification
DartNativeNotifications.show(
id: 'demo-default-1',
title: 'dartnative',
body: 'Hello from dartnative 👋 This is a default notification.',
payload: '{}',
);
show() posts straight to the platform’s notification service —
UNUserNotificationCenter on iOS, a NotificationCompat build on
Android — no scheduling ceremony for the immediate case. The id is a handle you can cancel()
later; the payload string round-trips untouched to your
setup(onTap:) callback when the user taps the banner — put your routing
information there, as JSON or whatever you like. Background the app after
tapping Show Default Notification to see the banner (a foregrounded
app shows notifications more quietly).
Step 4 — The chat-style banner
This is the show-off step, and it works on both platforms. On iOS 15+
these are Communication Notifications — the banner style Messages
and WhatsApp get, the sender’s avatar composed into the banner by iOS
via INSendMessageIntent. On Android the same call renders the sender
as the title with the avatar as a circle in the notification:
DartNativeNotifications.showChat(
id: 'demo-chat-1',
senderName: 'dartnative Bot',
body: 'Hello from dartnative 👋 This is a Communication Notification.',
avatar: avatar, // Uint8List — raw JPEG/PNG bytes
payload: '{}',
);
The tutorial fetches a real avatar over plain HttpClient and, if the
network fails, passes an empty Uint8List instead — the banner then
falls back to no custom avatar rather than blocking the send (on
Android that fallback is your launcher icon, so the avatar slot never
shows empty). On iOS versions before 15 it degrades to a plain
notification. It’s the single most recognizable “this app is
native” signal a chat app can send, and it’s one call.
Step 5 — The push token
The FCM registration token is the address your backend sends pushes to. Fetching it is one awaited call — the delegate you installed in Step 1 has been holding it since startup:
final token = await FirebaseMessaging.getToken();
If getToken times out: on iOS the diagnosis is almost always the Push
Notifications capability missing from the Runner target; on Android,
google-services.json missing from android/app/ or the Google
Services Gradle plugin not applied. The tutorial catches
TimeoutException and says so on screen.
Pushes that arrive while the app is open don’t bannerize; they stream
to Dart instead. The tutorial subscribes once in initState and logs
them live:
FirebaseMessaging.onMessage.listen((msg) {
setState(() {
_receivedMessages.insert(
0,
'[${msg.from}] ${msg.notificationTitle ?? ''}: '
'${msg.notificationBody ?? '(no body)'}',
);
});
});
Two channels, no overlap: onMessage is foreground arrivals,
setup(onTap:) is taps on banners (background or terminated). Send a
test push from the Firebase Console to the token from getToken and
watch it land in the list.
Why this is native
dartnative_firebasereplacesfirebase_messagingwith a zero-channel FFI implementation of the same job — the delegate is real APNs/FCM plumbing, not a Dart-side simulation. And the Communication banner is a genuinely native surface Flutter plugins don’t reach: avatar, sender identity, and system chat styling composed by iOS itself — and on Android by the platform’s own notification pipeline, zero-glue: the runtime dispatches permission results and tap intents to the plugin automatically.
The finished code
In the public repo — dn create ., dn run. The screen is the
playground’s notifications demo byte-identical (plus the playground’s
shared UI kit, also verbatim); lib/main.dart adds only the thin entry
from Step 1. When the playground screen improves, this tutorial inherits
it by copying the files again.