flutter_rich_notifications 0.1.3
flutter_rich_notifications: ^0.1.3 copied to clipboard
Push notifications with a full hero image AND full multi-line body, for Android (custom RemoteViews) and iOS (Notification Content Extension).
flutter_rich_notifications #
Display Android and iOS push notifications with a full hero image AND full multi-line body text — bypassing the body-line limits that stock BigPictureStyle (Android) and standard alert layouts (iOS) impose.
| flutter_local_notifications | awesome_notifications | flutter_rich_notifications | |
|---|---|---|---|
| Hero image | ✓ | ✓ | ✓ |
| Multi-line body alongside image | ✗ | ✗ | ✓ |
| iOS custom UI in expanded notification | ✗ | ✗ | ✓ (NCE) |
How it works #
| Platform | Mechanism |
|---|---|
| Android | Custom RemoteViews layout with setCustomBigContentView + DecoratedCustomViewStyle. Plugin downloads the image, builds the layout, and posts the notification. |
| iOS | Notification Content Extension (NCE) you add to your app. The NCE renders a programmatic UIViewController with UIImageView (hero) + multi-line UILabel (full body). The OS routes the notification to the NCE only when the APNs payload includes a matching aps.category. The category is optional — omit it and the notification still renders (title, body, and image via the NSE) using the system's default layout; you simply don't get the NCE's custom full-body UI. |
Installation #
dependencies:
flutter_rich_notifications: ^0.1.0
flutter pub get
Android setup #
Zero changes to your MainActivity — the plugin auto-registers its MethodChannel.
The plugin ships its own notification channel; users can customize the channel name via RichNotificationConfig (see Usage). If you want a custom small status-bar icon, add it under android/app/src/main/res/drawable/ and pass its name via androidSmallIconResName.
iOS setup #
iOS uses two Notification extensions (Apple's mechanism for app extensions). The plugin can't auto-create Xcode targets in your app, so you add them manually — 5–10 minutes total.
Step 1: Add the Notification Service Extension (handles image) #
- Open
ios/Runner.xcworkspacein Xcode (workspace, not xcodeproj). - File → New → Target → iOS → Notification Service Extension → Next.
- Product Name:
ImageNotification· Language: Swift · Project:Runner· Embed in Application:Runner→ Finish. - Cancel the "Activate scheme?" prompt.
- Replace the auto-generated
NotificationService.swiftwith the contents of:ios/.symlinks/plugins/flutter_rich_notifications/ios/ExtensionTemplates/NotificationServiceExtension/NotificationService.swift - Set the target's Minimum Deployments to match Runner (iOS 12.0 or whatever Runner uses).
Step 2: Add the Notification Content Extension (renders the rich UI) #
- File → New → Target → iOS → Notification Content Extension → Next.
- Product Name:
RichNotificationContent· Language: Swift · Project:Runner· Embed in Application:Runner→ Finish. - Cancel the "Activate scheme?" prompt.
- Delete the auto-generated
MainInterface.storyboard(we use programmatic UI). - Replace the auto-generated
NotificationViewController.swiftwith the contents of:ios/.symlinks/plugins/flutter_rich_notifications/ios/ExtensionTemplates/NotificationContentExtension/NotificationViewController.swift - Open the auto-generated
Info.plistand either:- Replace its contents with
ios/ExtensionTemplates/NotificationContentExtension/Info.plist, or - Set these keys manually in Xcode's plist editor:
NSExtension → NSExtensionAttributes → UNNotificationExtensionCategory=rich_notificationNSExtension → NSExtensionAttributes → UNNotificationExtensionInitialContentSizeRatio=1.0NSExtension → NSExtensionAttributes → UNNotificationExtensionDefaultContentHidden=YESNSExtension → NSExtensionAttributes → UNNotificationExtensionUserInteractionEnabled=NO
- Replace its contents with
- Set this target's Minimum Deployments to match Runner.
- Build the Runner scheme (⌘B) — both targets compile alongside.
Notification extensions don't render remote pushes in the iOS Simulator. Test on a real device.
Backend (FCM) payload #
Both platforms read the same FCM message, but each platform's "render rich" trigger is different — your backend sends both.
{
"message": {
"token": "<device_fcm_token>",
"notification": {
"title": "Payment Successful",
"body": "Hi Manish, your rent payment of ₹12,500 has been processed via UPI...",
"image": "https://example.com/receipt.jpg"
},
"data": {
"screen_route": "/post_payment_screen"
},
"android": { "priority": "high" },
"apns": {
"headers": { "apns-priority": "10" },
"payload": {
"aps": {
"mutable-content": 1,
"category": "rich_notification"
}
}
}
}
}
| Field | Required? | Why it's needed |
|---|---|---|
notification.image (or notification.android.image / apns.fcm_options.image) |
For images | Image URL; consumed by Android plugin and iOS NSE |
apns.payload.aps.mutable-content: 1 |
For images (iOS) | Triggers iOS NSE (image download) |
apns.payload.aps.category: "rich_notification" |
Optional | Triggers iOS NCE (rich full-body UI) — must match UNNotificationExtensionCategory in the NCE's Info.plist. Omit it and the notification still arrives with title/body/image in the system's default layout; only the NCE's custom UI is skipped. |
Minimal payload (no custom UI) #
Drop aps.category when you don't need the NCE's custom layout. The image still
attaches via the NSE, so iOS renders the stock expanded notification with the
hero image:
{
"message": {
"token": "<device_fcm_token>",
"notification": {
"title": "Payment Successful",
"body": "Your rent payment of ₹12,500 has been processed.",
"image": "https://example.com/receipt.jpg"
},
"apns": {
"headers": { "apns-priority": "10" },
"payload": { "aps": { "mutable-content": 1 } }
}
}
}
Drop mutable-content too for a plain text-only notification with no image.
Usage #
One-time configuration #
Optional. Defaults work out of the box.
import 'package:flutter_rich_notifications/flutter_rich_notifications.dart';
void main() {
FlutterRichNotifications.configure(
const RichNotificationConfig(
androidChannelId: 'payments',
androidChannelName: 'Payment Updates',
androidChannelDescription: 'Receipts, refunds, and reminders',
androidSmallIconResName: 'ic_notification', // optional, in res/drawable/
androidImageHeightDp: 220,
),
);
runApp(const MyApp());
}
Posting from FCM foreground handler #
When your app is in the foreground, FCM delivers the message to your Dart code — call show() to render the rich notification (Android only — iOS uses the NCE automatically).
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_rich_notifications/flutter_rich_notifications.dart';
FirebaseMessaging.onMessage.listen((message) {
final n = message.notification;
if (n == null) return;
FlutterRichNotifications.show(
title: n.title ?? '',
body: n.body ?? '',
imageUrl: n.android?.imageUrl ?? n.apple?.imageUrl,
screenRoute: message.data['screen_route'],
payload: message.data.map((k, v) => MapEntry(k, v.toString())),
);
});
Posting a local rich notification (no FCM) #
await FlutterRichNotifications.show(
title: 'Booking confirmed',
body: 'Your stay at Krishna Apartments from 12 Jun – 18 Jun is confirmed. '
'Check-in instructions have been sent to your email.',
imageUrl: 'https://cdn.example.com/booking-banner.jpg',
screenRoute: '/booking/123',
);
Customizing the iOS rich layout #
The NCE ships with a polished default — hero image (200pt rounded), title in semibold, body in regular with comfortable line spacing, semantic colors that adapt to light/dark mode.
To tweak it, open the NotificationViewController.swift you copied into your NCE target and edit the "Design knobs" block at the top of the class:
// Image-related knobs
var imagePosition: ImagePosition = .top // .top, .bottom, or .none
var imageHeight: CGFloat = 200
var imageCornerRadius: CGFloat = 12
// Typography
var titleFont: UIFont = .systemFont(ofSize: 17, weight: .semibold)
var bodyFont: UIFont = .systemFont(ofSize: 14, weight: .regular)
var bodyLineSpacing: CGFloat = 3
// Colors (defaults adapt to dark mode)
var titleColor: UIColor = .resolveLabel()
var bodyColor: UIColor = .resolveSecondaryLabel()
var cardBackgroundColor: UIColor = .clear
var accentColor: UIColor? = nil // set to a brand UIColor to show a left stripe
// Spacing
var horizontalPadding: CGFloat = 16
var verticalPadding: CGFloat = 14
var titleBodySpacing: CGFloat = 6
var imageToTextSpacing: CGFloat = 12
For deeper layout changes (e.g. custom typography per-notification based on payload data), override the open hooks:
override func applyTitle(_ text: String) {
titleLabel.attributedText = NSAttributedString(
string: text,
attributes: [.font: UIFont(name: "Inter-Bold", size: 18)!,
.kern: 0.3]
)
}
override func applyBody(_ text: String) {
super.applyBody(text)
// post-process bodyLabel further...
}
For a fully bespoke layout, override installConstraints() in a subclass — but at that point you're rebuilding the renderer, which is usually overkill.
Caveats #
| Platform | Caveat |
|---|---|
| Android (MIUI / HyperOS) | Xiaomi devices sometimes strip custom RemoteViews and fall back to their own template. The notification will still display correctly, just with the OEM layout. Stock Android / Pixel / Samsung / OnePlus / Realme render the custom layout as designed. |
| iOS heads-up banner | The brief banner that appears at the top of the screen when a notification arrives is system-controlled and shows the standard layout. The NCE only renders when the user long-presses / pulls down to expand. This is an Apple platform constraint, not a plugin limitation. |
| iOS Simulator | NCE doesn't fire for remote pushes in the simulator. Test on a real device. |
| Background / killed app | On both platforms, notifications delivered while the app is backgrounded / killed are routed by the OS, not the Flutter foreground handler. On iOS the NCE still fires for the rich layout. On Android, the plugin's show() won't run because Dart isn't executing — the system renders the standard notification. To get the rich layout in background/killed on Android too, send the FCM message as data-only (omit the notification block) and add a FirebaseMessaging.onBackgroundMessage handler that calls FlutterRichNotifications.show. |
License #
MIT. See LICENSE.