FCM in Android: Structured Push Notifications for High Retention
At You are launched, we believe that good engineering is synonymous with business velocity and product growth. Good engineering means creating product architectures that give speed and flexibility to marketing and product teams in experimentation, personalization of UX, and tweaking user journeys. And push notifications are one such critical element for mobile apps.
Yet, as Forrester publications reveal:
“Push notifications make the most of mobile marketing’s unique attributes: intimacy, immediacy, and context…. [yet] apps can’t afford to send thoughtless, frequent alerts to those who have allowed them to interrupt their daily lives.”
This is where default Firebase Cloud Messaging (FCM) hits the wall. Generally, the standard version of it with built-in customization used to be a good place to start for early-stage startups. However, today’s market with demanding and discerning users requires a more sophisticated approach. The tech solution is a custom in-product app architecture that will enable your product team to deliver:
- personalized (varied in layout and media),
- context-aware (sending if the user isn’t already browsing the target screen), and security-sensitive notifications, such as for banking apps.
In this article, we’ll showcase how structured FCM messages can enable business scaling through creating richer, context-aware user experiences and customization.
Table of contents
- Push Notifications: The Anatomy & Product Angle
- FCM Message Types and Why Defaults Fall Short
- The Difference between Standard FCM and Custom Data Message in Product Terms
- Push Notifications: Business Impact and Statistics
- Designing a Structured Notification Payload for Push Notifications
- Notification Channels & Delivery Helpers
- Implementing the Messaging Service
- Testing Without a Backend
- Step 3
- Conclusion
- FAQ: FCM in Android: Structured Push Notifications for High Retention
Push Notifications: The Anatomy & Product Angle
Below is an expanded push notification containing all major elements, some of which can be optional.

Basic Elements
The basic elements are icon (1), name (2), title (5), and message (6). Standard FCM payload contains only title (5) and message (6). The phone’s OS automatically pulls an icon (1) and the app’s name (2) from the app’s files installed on the phone. Moreover, the date/time appears automatically as well. While you can customize the format, the phone’s OS will attach the time of displaying the push notification, and not when the event that triggered it actually happened in your app.
Custom Elements
A thing like a timestamp is already a part where you will want to start customizing things. For example, if the user is on a subway without a signal, it may make more sense to display the timestamp of the actual app event, rather than when the phone got its signal back.
Then, (7) is a hero image or, if you want to implement a moving image, there is a GIF or an animated WebP file. Maybe your marketing team wants to include a pulsing ‘Limited time badge’. Or, what if you want to enrich UX by showing an icon of a delivery driver reaching the drop-off point, or a taxi driver pulling up at the requested pick-up point? This is also where a standard FCM will not suffice. It can only be implemented by injecting custom data.

The next point for going beyond standard FCM message is that, by default, tapping a push notification takes the user to your app. However, you can also include deep links to specific screens. For instance, if your user has added something to their cart but hasn’t completed a purchase, the ‘Complete Purchase’ CTA should be offered. Other options include: ‘Save for later’, ‘Shop now’, ‘Confirm’, ‘Claim offer’, etc. Here, too, the standard FCM notification payload falls short, as this should be managed through a data message from the app.
So, here is the contrast between the generic push notification (standard FCM) and the enriched push notification (custom data message).

FCM Message Types and Why Defaults Fall Short
So, FirebaseMessagingService (FCM) defines three types of messages:
- Notification Messages — Fully managed by FCM (standard).
- Notification + Data Messages — Hybrid, partly managed by FCM and partly by the app.
- Data Messages — Fully handled by the app, giving maximum flexibility and customization opportunities.
For structured notifications, Data Messages are the clear choice to enable custom handling. While it is the phone’s OS that handles standard FCM messages, data messages make sure to go through your app’s service. So, while the OS may suppress standard messages, data messages reach your app’s service regardless of app state and allow arbitrary payloads. What it means is that if your team implements the push notification via data messages, the process is as follows:
- First, wake up the app to get the relevant info,
- Register the notification with the OS to ensure its delivery, and,
- On top of that, make sure it does all background work outside the main thread.
Moreover, while out-of-the-box integration with FCM is smooth, limitations quickly appear once your use cases move beyond simple engagement notifications:
- Context awareness – You may want to suppress a chat notification if that chat is already open.
- Advanced system notifications – Android supports rich styles, inline actions, full-screen alerts, groups, and custom layouts. None of these is possible with FCM’s basic configuration.
- Beyond notifications – Push can update app state (e.g., moderation results, order confirmations) or refresh data in real time, all without websockets.
Notification Messages
FCM manages Notification Messages automatically. They serve simple use cases rendering the notification in the system’s notification tray without requiring custom handling in the application.
Key Characteristics:
- Automatic Display: The message renders in the notification tray without app-specific handling.
- Built-in Visual Representation: FCM manages the visual elements, including the title and body text.
- Foreground and Background Behaviour: The system handles these messages automatically when the app is in the background and only delegates to the app’s registered messaging service when the app is in the foreground, which limits our capability to intercept and take any custom actions.
- Custom data: Cannot contain custom key-value data
{ "token": "recipient_device_token", "notification": { "title": "Welcome Back!", "body": "We missed you! Check out what's new in the app." }}
Notification with Data Messages
This hybrid type includes both a notification block (handled automatically by FCM) and a data block (handled manually). This allows for some flexibility while still leveraging FCM’s automatic notification display.
Key Characteristics:
- Mixed Responsibility: The notification block is displayed automatically, while the data block is handled programmatically.
- Foreground and Background Behaviour: As with the notification messages, the app is only able to intercept it when it’s in the foreground. When the app is in the background, the service will not be triggered, and instead the system will handle the notification automatically, appending any custom data to the notification’s pending intent
- Custom data: Can contain simple key-value data
{ "token": "recipient_device_token", "notification": { "title": "Order Update", "body": "Your order #5678 has been shipped!" }, "data": { "orderId": "5678", "status": "shipped", "action": "view_order" }}
Data Messages
Data Messages consist of custom data in a flexible key-value format. They give you complete control over how the notification is processed and displayed. Not displayed by default.
Key Characteristics:
- Manual Handling: Developers must implement custom logic to process and display the message.
- Foreground and Background Processing: Data Messages are delivered to the messaging service regardless of the app state. This makes them an ideal candidate for our purposes.
- Custom data: Must contain simple key-value data
{ "token": "recipient_device_token", "data": { "title": "New Chat Message", "body": "You have a message from Alex.", "chatId": "12345", "action": "open_chat_screen" }}
In our implementation, we will focus exclusively on Data Messages, which provide greater flexibility and control over how we handle notifications in our application.
The Difference between Standard FCM and Custom Data Message in Product Terms
In product terms, a standard FCM notification can show:
“Title: Price Drop
Message: An item in your cart is now cheaper.”
Now, a custom data message can add richer data about the offered action:
“Title: Nike Air Max dropped in price
Message: Now $99 (was $120)
+ You can include an image of the item
+ a custom CTA “Buy Now” leading to checkout instead of just opening the app”
In this case, a custom data message wakes up the app to do several things that a standard FCM message cannot:
- Get specific information about the product;
- Run calculations of the price before and now;
- Get an image;
- Display a richer layout.
In addition, a custom data message can run more business logic than that:
- It can check the user profile about the size and show how many pairs are left in their size;
- Calculate the price differences in relative or absolute values.
“Title: Nike Air Max dropped in price
Message: Now $99, Save $21 (17% off)
Only 3 pairs left in your size”
Finally, another thing is what if the user is already on the checkout screen? Standard FCM message will still deliver the notification if the backend triggers ‘send the price drop notification’. It will happen even if the user is currently in the checkout, seeing the price drop, and is about to hit the Buy Now button. This makes it a distraction and annoyance for the user. A custom data message can check if the user is on the intended page and decide not to send the message.
Although you may not need all the customization options, a push notification like the one below is much more appealing than a generic one. Especially when its content aligns with the user’s preferences.

Summary: Standard FCM Message vs Structured Data Message
All in all, from the example above, here is a table that sums up the differences.
| Capability | Standard FCM | Custom Data Messages |
| Title/body notification | ✅ | ✅ |
| Product image | ❌ | ✅ |
| Exact price change | ❌ | ✅ |
| Inventory context (“3 left”) | ❌ | ✅ |
| Custom CTA buttons | ❌ | ✅ |
| Context awareness (user in checkout – decide NOT to show notification) | ❌ | ✅ |
Push Notifications: Business Impact and Statistics
Statistics reveal that even 1 generic push notification per week will force 10% of this app’s users to disable notifications, and the other 6% will uninstall the app. Another important point is that for a generic message, the baseline is often at 21% retention. Meaning that for an early-stage startup, a standard FCM message already boosts the retention metric. However, with an average of 46 push notifications daily per mobile user, there is definitely room for improvement.
In contrast, the data shows that every customization adds a boost to a certain business metric. For instance:
- For Android, the baseline reaction rate (when a user sees and clicks the notification) is at 4.6%. Personalisation can improve it 5x times. So now, 23% of users will react to a price drop on a specific item in their carts.
- The baseline for targeted push notifications is retention improvement by 39% over 11+ sessions. Advanced targeting adds 4x to retention improvement. It would mean that now 4x times more users book rides, complete purchases, use their virtual cards, share on social media, and whatever else the core action is that you target in your app.
- Simply adding an image or another rich format to your push notification adds 25% to reaction rates.
- Additionally, you can improve reaction rates by 40% using tailored times for push notification delivery.
Overall, implementing custom structured data messages helps startups to move the needle on key metrics such as increasing retention and engagement. Compared to standard FCM messages, custom data messages do so 4-5x more efficiently, which immediately translates to higher ROI and the app’s revenue.
Designing a Structured Notification Payload for Push Notifications
During our experimentation with notification data formats that would be easy to use and implement, but would be flexible enough to cover a wide range of use cases across different domains, we came up with the following format: structured JSON data as a notification’s payload field.
This approach allows us to harness the strength of kotlinx-serialization for the flexibility of data we are able to receive and have a generalised approach that simplifies further notification processing for notifications filtering, app state updates, etc.
The notification payload follows the following format:
@Serializabledata class NotificationPayload( @SerialName("body") val body: String?, @SerialName("entity_id") val entityId: Long?, @SerialName("entity_type") val entityType: NotificationEntityType?, @SerialName("notification_type") val notificationType: NotificationType?, @SerialName("title") val title: String?, @SerialName("details") val details: NotificationDetails?)@Serializableenum class NotificationEntityType { @SerialName("user") USER, @SerialName("chat") CHAT}@Serializableenum class NotificationType { @SerialName("new_follower") NEW_FOLLOWER, @SerialName("new_message") NEW_MESSAGE // And other notification types}@Serializable@Parcelizedata class NotificationDetails( @SerialName("sender_id") val chatMessageSenderId: Long?) : Parcelable
The notification payload consists of:
body– is notifications body plain texttitle– is notifications title plain textnotificationType– notification type, which is used for notifications filtering, notification channel managemententityType-entity type notification is referring to. It is used for deep linking to allow the addition of the new notification types on the server side that will open the correct app screen upon click, even when the user has an older app version installed, granted that the entityType is supported by the appentityId– entity ID which is used for deeplinkingdetails– data in variable format that can contain any number of additional fields. Primarily used for advanced deeplinking, when a simple entityId is not enough, and app state updates
Implementation Benefit 1: Business Agility with Push Notifications
This notification payload format allows us to have deep control over notification behaviour, but still support some of the automatic behaviour that is crucial for saving time when implementing new push notifications and backward compatibility with older app versions.
For instance, highly customized data messages still use Android’s automatic channel routing that specifies the behavior of push notifications. And it keeps doing it on the fly. In addition, customization doesn’t break interactions with OS packaging that does automatic things like assigning a unique ID, pulling the icon and app’s name, etc.
Implementation Benefit 2: Marketing Autonomy in Creating Push Notifications
Next, generally, when adding a new feature, it takes time to change code, test, and submit it for the app store review. However, the push notification code still also needs to go through the same process if you implement it with a standard FCM payload. However, our implementation removes the need to change app code if the marketing team wants to send a new type of push notifications.
Your marketing team generally uses marketing platforms like Braze, Customer.io, OneSignal, or an internal admin dashboard. They create a new push notification campaign using these friendly interfaces. Usually, they will fill out some forms and then, when they hit ‘create’, there is parsing of this data into JSON. The JSON format comes from our implementation here. Then, this JSON payload goes to FCM using APIs. After that, it reaches the phone’s OS, which wakes up the app to do the work in the background. Finally, the user gets the rendered push notification.
Therefore, notificationType & details give your product & marketing teams the autonomy to launch a highly-targeted and customized campaign, all without waiting for a development team to code those up. So, the marketing team uses their usual tools, no need to add code through developers or wait for the app store to review it, and there is no limit on the number of campaigns.
Implementation Benefit 3: Safety & Loss Prevention
In terms of safety, this implementation features entityType and entityId. For mobile apps, not all users have the latest version of your app. Some users might still run a several-month-old version that does not have certain features for which you are going to trigger a sales funnel. So, when you send an Auction notification (the new feature, released 2 weeks ago), the standard FCM implementation will crash the app if the user taps. However, providing type and ID decouples the info and leads to a graceful and smooth app behavior. The app will check if the user has a certain feature. If not, it will reroute the notification to a generic view engine, and the app will append entityId to create a URL that will work out for every version. Safeguarding against crashes of app’s older versions prevents:
- Loss of revenue by fixing a broken link when the user taps to follow a sales channel;
- Loss of CAC by preventing an app’s ungraceful crash that might push users who haven’t been active to delete the app;
- Loss of reputation on App Store by eliminating the possibility of users (whose app crashed) leaving a 1-star review.
Notification Channels & Delivery Helpers
Notification channels allow your app to create ‘sub-categories,’ so that when the user opts out of notifications, they can do it for just a certain notification type and not all of them. For instance, they might opt out of Promotion notifications, but stay opted in for Transactional (resulting from their actions, changes in delivery, balance, etc.). Or, they might opt out of all categories individually. Then, if you create a new notification channel, like an Auction category, they will receive this one. Though this is true only if they haven’t applied the global ‘opt-out’ for all notifications, e.g., used a ‘master’ toggle for the entire app.
Additionally, the notificationType field has a pre-set list of notification categories (or, in Android terms, ‘channels’). For instance, these can be promotional, transactional, urgent, community, etc. When a marketing team decides to launch an immediate Promotional campaign, they simply need to pass the value ‘promotional,’ and this will define a set of behaviors. For instance, it should be delivered within specific hours, not wake up the user after 10 PM, use a quiet chime sound, and be grouped with other sales notifications.
To follow SOLID principles and ensure separation of concerns, we introduced a NotificationsHelper class for:
- Creating and managing notification channels.
- Sending notifications in the app’s consistent style.
- Resetting channels and tokens on logout.
We then layered on a FilteredNotificationSender that lets each screen apply filters to suppress irrelevant notifications (e.g., muting active chats).
This design keeps notification delivery centralized, testable, and lifecycle-aware.
NotificationHelper Implementation
enum class NotificationChannelType(val channelId: String, @StringRes val title: Int) { NEW_FOLLOWER(channelId = "new_follower", title = LocalizableResources.string.notification_channel_new_follower), CHAT_MESSAGES( channelId = "chat_messages", title = LocalizableResources.string.notification_channel_chat_messages ), OTHER("other", LocalizableResources.string.notification_channel_other)}fun NotificationType?.toNotificationChannelType() = when (this) { NotificationType.NEW_FOLLOWER -> NotificationChannelType.NEW_FOLLOWER NotificationType.NEW_MESSAGE -> NotificationChannelType.CHAT_MESSAGES null -> NotificationChannelType.OTHER}class NotificationsHelper @Inject constructor( @ApplicationContext private val context: Context, @ApplicationResources private val resources: AppResources) { fun closeNotificationsChannels() { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager NotificationChannelType.entries.forEach { channel -> notificationManager.deleteNotificationChannel(channel.channelId) } Firebase.messaging.deleteToken() } fun createNotificationChannels() { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager NotificationChannelType.entries.forEach { channel -> val notificationChannel = NotificationChannel( channel.channelId, resources.getString(channel.title), NotificationManager.IMPORTANCE_HIGH ) notificationManager.createNotificationChannel(notificationChannel) } } fun sendNotification(notificationPayload: NotificationPayload, @DrawableRes icon: Int) { val resultIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP notificationPayload.notificationType?.let { notificationType -> putNotificationData( notificationType = notificationType, notificationEntityType = notificationPayload.entityType, notificationEntityId = notificationPayload.entityId, notificationDetails = notificationPayload.details ) } } val pendingIntent = PendingIntent.getActivity( context, UUID.randomUUID().mostSignificantBits.toInt(), resultIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) val notificationBuilder = NotificationCompat.Builder( context, notificationPayload.notificationType.toNotificationChannelType().channelId ) .setSmallIcon(icon) .setContentTitle(notificationPayload.title) .setContentText(notificationPayload.body) .setAutoCancel(true) .setOnlyAlertOnce(true) .setContentIntent(pendingIntent) .setDefaults(Notification.DEFAULT_ALL) .setCategory(NotificationCompat.CATEGORY_SOCIAL) val notificationManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager notificationManager.notify(UUID.randomUUID().mostSignificantBits.toInt(), notificationBuilder.build()) } private fun Intent.putNotificationData( notificationType: NotificationType, notificationEntityType: NotificationEntityType?, notificationEntityId: Long?, notificationDetails: NotificationDetails? ) = apply { putExtra(NOTIFICATION_TYPE, notificationType) putExtra(NOTIFICATION_ENTITY_TYPE, notificationEntityType) putExtra(NOTIFICATION_ENTITY_ID, notificationEntityId) putExtra(NOTIFICATION_DETAILS, notificationDetails) } companion object { const val NOTIFICATION_ENTITY_TYPE = "notification_entity_type_extra" const val NOTIFICATION_ENTITY_ID = "notification_entity_id_extra" const val NOTIFICATION_TYPE = "notification_type_extra" const val NOTIFICATION_DETAILS = "notification_details_extra" const val NO_ENTITY_ID_CODE = -1L }}
The helper class contains the following methods:
createNotificationChannelscreates all notification channels of the app and must be invoked before the app tries to send a system notificationcloseNotificationsChannelscloses all notification channels and resets the app’s FCM token. Must be invoked on user logout to stop any incoming push notificationssendNotificationtakes in a notification payload and icon resource ID. Creates and sends a notification in the app’s default style and packages any data that is present in the notification payload and packages in into a pending intent for deeplinking.
Delivery Helper Implementation
In addition to NotificationsHelper, there is a FilteredNotificationSender singleton to support conditional system notification sending. Its purpose is to filter out any incoming notification send request according to its active filters.
It is a responsibility of each particular screen to set and remove its set of filters depending on its lifecycle state.
The class allows applying filters depending on the notification type and optionally an entity ID. When no entityId is supplied, all notifications of the particular type will be filtered out.
sendNotificationsends a notification if it’s not a subject of active filtersaddNotificationFiltersadds a set of notification filtersremoveNotificationFiltersremoves a set of notification filters
data class NotificationFilter( val type: NotificationType, val entityId: Long? = null)@Singletonclass FilteredNotificationSender @Inject constructor(private val notificationsHelper: NotificationsHelper) { private var activeNotificationFilters = mutableSetOf<NotificationFilter>() fun sendNotification(notificationPayload: NotificationPayload, @DrawableRes icon: Int) { if (!shouldFilterNotification(notificationPayload)) { notificationsHelper.sendNotification(notificationPayload, icon) } } fun addNotificationFilters(vararg filters: NotificationFilter) { activeNotificationFilters.addAll(filters) } fun removeNotificationFilters(vararg filters: NotificationFilter) { activeNotificationFilters.removeAll(filters.toSet()) } private fun shouldFilterNotification(notificationPayload: NotificationPayload) = activeNotificationFilters.any { filter -> notificationPayload.notificationType == filter.type && (filter.entityId == null || notificationPayload.entityId == filter.entityId) }}
Implementing the Messaging Service
Now that we have all the moving parts for system notification creation and push data parsing, it’s time to implement the messaging service. Following the official guide, create and register a custom Firebase messaging service. It is also necessary to inject all of the necessary dependencies through dependency injection and prepare for asynchronous work using Kotlin coroutines.
The dependencies include:
createRemoteDeviceUseCase– use case for sending FCM token and userId to our servernotificationsHelper– notification helper for notification channel creationnotificationSender– notification sender, a proxy for sending any received notificationjson– json instance for parsing incoming notification payload datacoroutineScope– coroutine scope for executing suspend operations, which is closed when the process is destroyed
@AndroidEntryPointclass TrublaFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var createRemoteDeviceUseCase: CreateRemoteDeviceUseCase @Inject @IoDispatcher lateinit var coroutineDispatcher: CoroutineDispatcher @Inject lateinit var notificationsHelper: NotificationsHelper @Inject lateinit var notificationSender: FilteredNotificationSender private val json by lazy { Json { ignoreUnknownKeys = true explicitNulls = false coerceInputValues = true } } private val coroutineScope = CoroutineScope(SupervisorJob()) override fun onNewToken(token: String) { super.onNewToken(token) // TODO: Send token } override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) // TODO: Process push } override fun onDestroy() { super.onDestroy() coroutineScope.cancel() }}
Now, let’s implement the service’s functionality.
onNewToken(). This method triggers whenever Firebase Cloud Messaging (FCM) generates a new token for the device. This token is a unique identifier for targeting the device for push notifications. The method ensures the dispatch of the new token to our server to keep the backend updated.
override fun onNewToken(token: String) { super.onNewToken(token) coroutineScope.launch(coroutineDispatcher) { createRemoteDeviceUseCase( fcmToken = token ) }}
onMessageReceived(message: RemoteMessage). This method triggers when FCM receives a new message. It is responsible for processing the incoming message, extracting relevant data from the RemoteMessage entity, and creating notification channels to display the notification appropriately. The typical implementation would look like this:
override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) coroutineScope.launch(coroutineDispatcher) { notificationsHelper.createNotificationChannels() val payload = message.data[PUSH_PAYLOAD_KEY] val notificationPayload: NotificationPayload? = runCatching { payload?.let { json.decodeFromString<NotificationPayload>(it) } }.getOrNull() notificationPayload?.let { notificationSender.sendNotification( notificationPayload = notificationPayload, icon = R.drawable.img_logo_vector ) } }}companion object { private const val PUSH_PAYLOAD_KEY = "payload"}
The method receives a push notification message from FCM, creates notification channels, and then parses the notification payload using kotlinx-serialization. In case of success, it delegates the creation of the actual system notification to the filtered notification sender described earlier.
It’s also possible, if necessary, to add any additional actions into onMessageReceived depending on the parsed payload for updating the app’s state, scheduling background work, etc.
⚠️ In order to receive notifications, don’t forget to send the FCM push token to your backend server upon user login and request push notification permission on Android 13 and newer, according to FCM documentation and Android Developers documentation
Testing Without a Backend
To test structured notifications locally:
Step 1
Acquire a Google Cloud access token with Firebase Cloud Messaging API v1 from the OAuth playground
Step 2
Retrieve the device’s FCM messaging token, which is usually sent to the backend server following FCM documentation
Step 3
Using curl or a graphical utility like Postman, send a POST request to the FCM API using the access token. In the example, replace:
YOUR_PROJECT_NAMEwith the name of the Google Cloud project attached to the Firebase appYOUR_ACCESS_CODEwith an access code with FCM API authorizationYOUR_FCM_DEVICE_TOKENwith the device FCM token, which is usually sent to the backend server- Any placeholder values of the payload
curl --location 'https://fcm.googleapis.com/v1/projects/<YOUR_PROJECT_NAME>/messages:send' \--header 'Authorization: Bearer <YOUR_ACCESS_CODE>' \--header 'Content-Type: application/json' \--data '{ "message": { "token": "<YOUR_FCM_DEVICE_TOKEN>", "data": { "payload": "{\"entity_id\": \"<ENTITY_ID>\",\"entity_type\": \"<ENTITY_TYPE>\",\"notification_type\": \"<NOTIFICATION_TYPE>\",\"title\": \"<TITLE>\",\"body\": \"<BODY>\"}" } }}'
Conclusion
Proper engineering implementation of the push infrastructure that we offer is a valuable business asset. It enables dynamic, targeted marketing and is instrumental for driving retention. From an Android Development perspective, having a clean, decoupled, life-cycle aware infrastructure early helps to protect the codebase from technical debt, unnecessary code changes, and time-consuming app store code reviews.
Structured push notifications unlock the full potential of Firebase Cloud Messaging. By relying on Data Messages and a well-defined JSON payload, you can:
- Deliver notifications that adapt to user context.
- Maintain backwards compatibility with older app versions.
- Extend push beyond engagement into real-time app state updates.
At You Are Launched, we’ve found this approach scales smoothly across projects, keeping notification handling both powerful and maintainable. If you’re hitting the ceiling of FCM’s default tools, structured push notifications are the way forward.
FAQ: FCM in Android: Structured Push Notifications for High Retention
It means the app checks what the user is doing before sending a notification. If the user is already in a chat, there is no reason to send a new message alert for that same chat. This prevents annoying interruptions and makes the app feel smarter.
Notification channels let users choose which types of alerts they want to receive. Someone might turn off promotional messages but keep order updates. This gives users more control and reduces the chance they disable all notifications at once.
The marketing team can create new notification campaigns without asking developers for help. They use tools like Braze or OneSignal, fill out a form, and the rest is handled automatically. No app store review is needed for new campaign types.
Every device gets a unique FCM token that tells the backend where to send notifications. When the token changes, the backend needs to be updated right away. If the token is outdated, notifications will not reach the user at all.
Developers can test notifications locally using a Google Cloud access token and a tool like Postman. This lets the team check how the notification looks and behaves before connecting a real backend. It saves time and catches bugs early.