Beyond Basic Text Notifications
A plain title-and-body notification is the starting point, not the destination. Rich notifications — images, action buttons, and deep links straight to relevant content — meaningfully improve engagement when used deliberately.
Sending an Image Notification
await admin.messaging().send({
token: userToken,
notification: {
title: 'Your order has shipped!',
body: 'Track your package in real time.',
},
android: {
notification: {
imageUrl: 'https://cdn.example.com/order-shipped.png',
},
},
apns: {
fcmOptions: {
imageUrl: 'https://cdn.example.com/order-shipped.png',
},
},
});
Adding Action Buttons (Android)
await admin.messaging().send({
token: userToken,
notification: { title: 'New message from Sam', body: 'Hey, are you free tonight?' },
android: {
notification: {
clickAction: 'OPEN_CHAT',
channelId: 'messages',
},
},
data: { screen: 'chat', chatId: '4821' },
});
// Registering the notification channel with actions (Android)
class MessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
val intent = Intent(this, ChatActivity::class.java).apply {
putExtra("chatId", message.data["chatId"])
}
val replyAction = NotificationCompat.Action.Builder(
R.drawable.ic_reply, "Reply", buildReplyPendingIntent(message)
).build()
val notification = NotificationCompat.Builder(this, "messages")
.setContentTitle(message.notification?.title)
.setContentText(message.notification?.body)
.addAction(replyAction)
.setContentIntent(PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE))
.build()
NotificationManagerCompat.from(this).notify(message.hashCode(), notification)
}
}
Deep Linking to the Right Screen
// Flutter: handling notification taps and routing
FirebaseMessaging.onMessageOpenedApp.listen((message) {
final screen = message.data['screen'];
final id = message.data['chatId'];
switch (screen) {
case 'chat':
Navigator.pushNamed(context, '/chat', arguments: id);
break;
case 'orderDetails':
Navigator.pushNamed(context, '/orders/${message.data['orderId']}');
break;
}
});
Handling Cold-Start Deep Links
When a user taps a notification and the app wasn’t running, you need to capture the initial message separately from the foreground/background listeners:
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
handleNotificationNavigation(initialMessage.data);
}
Silent Data-Only Notifications
For background sync without displaying anything to the user (updating cached data, refreshing a badge count), send a data-only payload with no notification block:
await admin.messaging().send({
token: userToken,
data: { type: 'sync', updatedAt: Date.now().toString() },
android: { priority: 'high' },
apns: { headers: { 'apns-priority': '5' } },
});
Conclusion
Rich notifications and reliable deep linking are what separate a genuinely useful notification system from one users learn to ignore. Design the data payload carefully upfront — it’s what determines whether a tap actually lands the user somewhere relevant.