FlutterFlow uses Firebase Cloud Messaging for push notifications. You can customize notification appearance by structuring your FCM payload with title, body, image, and data fields. Add Android notification channels for categorization, configure custom sounds, and use the onMessage handler to intercept foreground notifications and display custom in-app UI.
Beyond the Default Notification
FlutterFlow's built-in Send Push Notification action sends a basic FCM message — but FCM supports far richer payloads. By structuring your notification payload carefully and adding custom code for channel setup and foreground handling, you can create notifications that feel native to your brand: category-specific sounds, large images, deep-link action buttons, and in-app banners that match your UI instead of the OS default.
Prerequisites
- FlutterFlow project with Firebase connected (Settings > Firebase)
- Push Notifications enabled in Settings > Push Notifications
- FCM token stored in your users Firestore document
- Android notification channel concept understanding (optional but helpful)
Step-by-step guide
Define Android Notification Channels in Custom Code
Define Android Notification Channels in Custom Code
Android 8+ requires notification channels before you can display categorized notifications. In FlutterFlow, open Custom Code > Custom Actions and create a new action called initNotificationChannels. This Dart code runs once at app startup and registers channels like 'orders', 'promotions', and 'alerts' with distinct importance levels. Each channel can have a different sound, vibration pattern, and lock-screen visibility. Without channels, all your notifications land in the default bucket with no customization possible.
1import 'package:flutter_local_notifications/flutter_local_notifications.dart';23Future<void> initNotificationChannels() async {4 final FlutterLocalNotificationsPlugin plugin =5 FlutterLocalNotificationsPlugin();67 const AndroidNotificationChannel ordersChannel = AndroidNotificationChannel(8 'orders',9 'Order Updates',10 description: 'Notifications about your order status',11 importance: Importance.high,12 sound: RawResourceAndroidNotificationSound('order_chime'),13 );1415 const AndroidNotificationChannel promoChannel = AndroidNotificationChannel(16 'promotions',17 'Promotions',18 description: 'Deals and promotional offers',19 importance: Importance.low,20 );2122 await plugin23 .resolvePlatformSpecificImplementation<24 AndroidFlutterLocalNotificationsPlugin>()25 ?.createNotificationChannel(ordersChannel);2627 await plugin28 .resolvePlatformSpecificImplementation<29 AndroidFlutterLocalNotificationsPlugin>()30 ?.createNotificationChannel(promoChannel);31}Expected result: On Android, your app's notification settings screen shows separate 'Order Updates' and 'Promotions' channels users can toggle independently.
Structure Your FCM Payload for Rich Notifications
Structure Your FCM Payload for Rich Notifications
When sending notifications via your Cloud Function or the Firebase Console, structure the payload to include both the notification object (for system tray display) and a data object (for your app logic). The notification object holds title, body, and image. The data object carries action type, target page, and any IDs your app needs for deep linking. Keeping these two objects separate ensures the notification displays correctly even when the app is closed, while your app still receives the data payload to navigate correctly when tapped.
1// Cloud Function payload example2const payload = {3 notification: {4 title: 'Your order has shipped!',5 body: 'Estimated delivery: Tomorrow by 5pm',6 image: 'https://yourcdn.com/shipping-icon.png'7 },8 data: {9 action: 'navigate',10 target_page: 'OrderDetail',11 order_id: orderId,12 channel_id: 'orders'13 },14 android: {15 notification: {16 channel_id: 'orders',17 sound: 'order_chime',18 click_action: 'FLUTTER_NOTIFICATION_CLICK'19 }20 },21 apns: {22 payload: {23 aps: {24 sound: 'order_chime.aiff',25 badge: 126 }27 }28 },29 token: userFcmToken30};Expected result: Notifications arrive with the shipping image visible in the system tray and tapping them passes the order_id data to your app.
Add Custom Sounds to iOS and Android
Add Custom Sounds to iOS and Android
For iOS, place your .aiff sound file inside the Runner/Resources folder in your downloaded Flutter project, then re-upload via code export. For Android, place your .mp3 or .wav file in android/app/src/main/res/raw/. In FlutterFlow, go to Custom Files and upload the sound assets, specifying the correct platform path. Reference the sound by filename (without extension) in your Android channel setup and FCM payload. On iOS, the sound name in the APNS payload must exactly match the filename including the .aiff extension.
Expected result: Order notifications play your custom chime sound while promotional notifications use the softer default tone.
Handle Foreground Notifications with a Custom In-App Banner
Handle Foreground Notifications with a Custom In-App Banner
By default, FCM does not show a system notification when your app is in the foreground — it only delivers the message silently to your code. To show something to the user, add a Custom Action that subscribes to FirebaseMessaging.onMessage stream. When a message arrives, update a Page State variable (e.g., activeNotificationBanner) with the notification content. Wire this state variable to a conditionally visible banner widget at the top of your main scaffold. Use an AnimationController to slide the banner in, auto-dismiss it after 4 seconds, and let the user tap it to navigate to the target page.
1import 'package:firebase_messaging/firebase_messaging.dart';23void listenForegroundMessages(4 Future<void> Function(Map<String, dynamic> data) onMessage,5) {6 FirebaseMessaging.onMessage.listen((RemoteMessage message) async {7 final data = {8 'title': message.notification?.title ?? '',9 'body': message.notification?.body ?? '',10 'action': message.data['action'] ?? '',11 'target_page': message.data['target_page'] ?? '',12 'payload': message.data,13 };14 await onMessage(data);15 });16}Expected result: When a notification arrives while the app is open, a branded banner slides down from the top of the screen instead of the generic OS notification shade.
Add Action Buttons to Android Notifications
Add Action Buttons to Android Notifications
Android supports up to three action buttons on notifications without the user needing to open the app. In your Android channel-aware payload, add an android.notification.actions array with title and action fields. In your app's notification tap handler (FirebaseMessaging.onMessageOpenedApp), check the action field on the message to determine which button was tapped and execute the right logic — for example 'Mark as Read' vs 'View Details'. Register these action categories in your custom notification setup code.
Expected result: Order notifications show 'Track Order' and 'Dismiss' buttons directly in the notification shade — users take action without launching the app.
Complete working example
1import 'package:firebase_messaging/firebase_messaging.dart';2import 'package:flutter_local_notifications/flutter_local_notifications.dart';34// ─── Channel Definitions ────────────────────────────────────────────────────56const AndroidNotificationChannel ordersChannel = AndroidNotificationChannel(7 'orders',8 'Order Updates',9 description: 'Real-time updates about your orders',10 importance: Importance.high,11 sound: RawResourceAndroidNotificationSound('order_chime'),12 enableVibration: true,13);1415const AndroidNotificationChannel promoChannel = AndroidNotificationChannel(16 'promotions',17 'Promotions',18 description: 'Deals and special offers',19 importance: Importance.low,20 playSound: false,21);2223const AndroidNotificationChannel alertsChannel = AndroidNotificationChannel(24 'alerts',25 'Important Alerts',26 description: 'Security and account alerts',27 importance: Importance.max,28 enableVibration: true,29);3031// ─── Initialization ──────────────────────────────────────────────────────────3233Future<void> initNotificationChannels() async {34 final FlutterLocalNotificationsPlugin plugin =35 FlutterLocalNotificationsPlugin();3637 final androidImpl = plugin.resolvePlatformSpecificImplementation<38 AndroidFlutterLocalNotificationsPlugin>();3940 await androidImpl?.createNotificationChannel(ordersChannel);41 await androidImpl?.createNotificationChannel(promoChannel);42 await androidImpl?.createNotificationChannel(alertsChannel);4344 // Request iOS permissions45 await FirebaseMessaging.instance.requestPermission(46 alert: true,47 badge: true,48 sound: true,49 provisional: false,50 );5152 // Handle background tap — navigate when app is terminated53 final RemoteMessage? initial =54 await FirebaseMessaging.instance.getInitialMessage();55 if (initial != null) {56 _handleNotificationTap(initial.data);57 }5859 // Handle background → foreground tap60 FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {61 _handleNotificationTap(message.data);62 });63}6465// ─── Navigation Handler ──────────────────────────────────────────────────────6667void _handleNotificationTap(Map<String, dynamic> data) {68 final String targetPage = data['target_page'] ?? '';69 final String id = data['id'] ?? '';70 // Dispatch to your FlutterFlow navigation action71 // using FFAppState or a navigation helper72 FFAppState().pendingNavPage = targetPage;73 FFAppState().pendingNavId = id;74}Common mistakes when creating Custom Push Notifications in FlutterFlow
Why it's a problem: Sending all notifications on the default channel with no categorization
How to avoid: Create distinct Android notification channels for each category (orders, promos, alerts) so users control each independently from their device settings.
Why it's a problem: Not handling the onMessageOpenedApp vs getInitialMessage distinction
How to avoid: Always call getInitialMessage() during app initialization AND listen to onMessageOpenedApp to cover both cold-start and background-to-foreground scenarios.
Why it's a problem: Placing notification logic inside a FlutterFlow page instead of a persistent service
How to avoid: Register all FCM listeners in main.dart or a top-level Custom Action called once at app launch, outside any specific page.
Why it's a problem: Using the same sound file name on iOS and Android without format conversion
How to avoid: Convert your sound to both .aiff (iOS) and .mp3 (Android) and reference the correct filename with extension in each platform's notification payload.
Best practices
- Always request notification permission with a contextual explanation — explain why your app needs notifications before triggering the OS permission dialog.
- Store FCM tokens in Firestore and refresh them using FirebaseMessaging.instance.onTokenRefresh to handle token rotation.
- Limit notification frequency: more than 3 per day for non-critical categories causes users to disable all notifications.
- Always include a data payload alongside the notification object so your app can deep-link correctly regardless of whether it was open, backgrounded, or closed.
- Use collapse_key in your FCM payload so that multiple unread notifications of the same type stack as one entry in the notification shade.
- Test notifications on both iOS simulator (requires physical device for actual delivery) and Android emulator — behavior differs significantly.
- Log notification open events to Firestore so you can measure click-through rate by channel and refine your messaging strategy.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a FlutterFlow app with Firebase. Explain how to create Android notification channels in Dart, handle FCM foreground messages with a custom in-app banner, and structure an FCM payload that includes a large image and action buttons. Show code for all three.
In my FlutterFlow app, add a Custom Action that initializes three Android notification channels (orders, promotions, alerts) with different importance levels. Then add a foreground message listener that updates a Page State variable called activeNotification with the message title, body, and target page for in-app banner display.
Frequently asked questions
Does FlutterFlow support notification action buttons out of the box?
No. FlutterFlow's built-in Send Push Notification action sends a basic title and body only. Action buttons require a custom FCM payload sent from a Cloud Function and a Custom Action in your app to handle the button tap responses.
Why does my notification image not appear on iOS?
iOS requires a Notification Service Extension to download and display images in rich notifications. Without this extension, the image field is ignored. You need to add the extension via Xcode after exporting your FlutterFlow project as code.
Can I send notifications without a backend Cloud Function?
You can send test notifications from the Firebase Console. For production use, you should always use a server-side Cloud Function with your service account credentials — never embed your FCM server key in client code.
What is the difference between notification payload and data payload in FCM?
The notification payload is handled by the OS and displays automatically in the system tray even when your app is closed. The data payload is delivered silently to your app code for processing. Best practice is to include both: notification for display, data for deep-linking logic.
How do I test custom notification sounds before releasing my app?
On Android, place your sound file in android/app/src/main/res/raw/ and send a test notification from the Firebase Console with the channel ID specified. On iOS, you need a physical device — the simulator cannot play custom notification sounds.
Why are my foreground notifications not showing anything?
FCM suppresses system tray notifications when your app is in the foreground. You must listen to the FirebaseMessaging.onMessage stream and build your own in-app notification UI — a sliding banner, a badge update, or a dialog — to alert the user.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation