← All

Android push notifications setup for solo developers

Android push notifications setup for solo developers

You have an Android app that needs push notifications, and you do not have a team to figure it out. Deprecated APIs and Android 13's new notification runtime permission already compete for your attention. A Google Play deadline adds another constraint.

A 25% usage decrease in mobile apps by 2027 raises the bar for every notification you send as AI assistants replace routine app functions. A 70% of consumers finding shows many people wish they spent less time on their devices. A direct FCM integration keeps the stack small, avoids unnecessary dependencies, and gives you control over delivery.

Why FCM direct is the right starting point

FCM is a practical starting point if you need Android push delivery without adding more services. It gives you control over sends and lets you postpone extra tooling until you actually need it.

FCM handles message delivery at no cost on both the Spark and Blaze Firebase plans. You get device token targeting and the HTTP v1 API for programmatic sends.

One warning matters before you start: the legacy FCM HTTP API using Authorization: key=AIzaSy... headers was shut down in 2024. The current API requires a short-lived OAuth 2.0 access token sent as a Bearer token.

Firebase project setup and your first test notification

Most FCM setup problems come from Gradle and manifest configuration, or from missing or misconfigured notification channels. Get those right first, then test on a real device to confirm the full delivery path.

Console and Gradle configuration

Your app can not register for messaging until Firebase is connected correctly.

  1. Go to Firebase console and create a new project.
  2. Click Add app, select Android, and enter your package name.
  3. Download google-services.json and place it in your /app directory.
  4. Add the Google services plugin to your project-level build.gradle.kts:
plugins {
    id("com.google.gms.google-services") version "4.4.4" apply false
}
  1. Add the Firebase BoM and messaging dependency to your app-level build.gradle.kts:
plugins {
    id("com.google.gms.google-services")
}
dependencies {
    implementation(platform("com.google.firebase:firebase-bom:34.14.0"))
    implementation("com.google.firebase:firebase-messaging")
}

The Firebase BoM manages SDK versions, so you do not need to specify each one separately.

Manifest and notification channel

Android will not display FCM notifications correctly without the manifest entry and a notification channel.

Your AndroidManifest.xml needs the POST_NOTIFICATIONS permission declaration and a default notification channel ID:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application ...>
    <meta-data
        android:name="com.google.firebase.messaging.default_notification_channel_id"
        android:value="@string/default_notification_channel_id" />
</application>

Create the channel itself in Application.onCreate() or MainActivity.onCreate(). Channels are mandatory on supported Android versions. Calling createNotificationChannel() with an existing ID is a safe no-op.

Sending a test notification

Run a real-device test as soon as the app is configured to confirm Firebase can reach the device and Android can display what arrives.

Install your app on a Play services device. Place the app in the background. Copy the FCM registration token from logcat, then go to Firebase Console → DevOps & Engagement → Messaging → Create your first campaign → Firebase In-App messages → Create.

Paste the token, enter test text, and send. The workflow for obtaining a registration token and sending a test notification is documented in the FCM client guide.

If that test works, the core delivery path is running end to end.

Runtime permissions on Android 13 and above

On Android 13 and above, users will not see notifications unless you request POST_NOTIFICATIONS at the right time. The manifest declaration alone is not enough.

Android 13 introduced POST_NOTIFICATIONS as a runtime permission, similar to camera and location permissions. You must request it programmatically.

The official Android documentation states explicitly: let users familiarize themselves with your app before asking. Prompting on first launch, before users understand the value of your notifications, often leads to a denial.

The permission request flow

Ask only when the user understands why notifications matter. That timing keeps you aligned with Android guidance and gives you a better chance of a useful opt-in.

private val requestPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission(),
) { isGranted: Boolean ->
    if (isGranted) {
        // FCM can post notifications
    } else {
        // Inform user about disabled notifications
    }
}

private fun askNotificationPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        if (ContextCompat.checkSelfPermission(
                this, Manifest.permission.POST_NOTIFICATIONS
            ) == PackageManager.PERMISSION_GRANTED) {
            // Already granted
        } else if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
            // Show rationale UI, then request
        } else {
            requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
        }
    }
}

Targeting API 33+ gives you control over when the system dialog appears. You can show an in-app explanation first. If the user declines your in-app screen, the system dialog never fires.

The permissions guidance is clear on two points: do not block the UI with a full-screen interstitial that forces permission, and do not nag users who already declined. After a denial, explain what functionality is unavailable.

Mandatory compliance for Android 14 and 15

Recent Android changes can break notifications, background work, or your next Play submission. Check target SDK rules and foreground service behavior, then review newer restrictions around DND and overlays.

Starting August 31, 2025, new apps and updates submitted to Google Play must target API 35 or higher. Recent permission and service changes apply to your next submission.

Android 14: foreground service types

Apps targeting Android 14 must declare a service type in the manifest, and most types also require a matching permission. Missing this causes a SecurityException at runtime.

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<service
    android:name=".MyService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="false"/>

At runtime, pass the type to startForeground():

startForeground(NOTIFICATION_ID, notification,
    ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)

If your app starts foreground services, test this path before you ship.

Android 15: Do Not Disturb and overlay changes

Apps targeting API 35 can no longer modify the global DND state directly. For apps targeting Android 15, use AutomaticZenRule instead of directly relying on setInterruptionFilter() and setNotificationPolicy().

Holding SYSTEM_ALERT_WINDOW alone also no longer allows background foreground service starts. A visible overlay window must be active.

These changes matter if your app touches DND behavior or starts services from the background.

A crash pattern to avoid

Do not gate startForeground() behind areNotificationsEnabled(). Call startForeground() unconditionally when the service starts. The system displays the service in the Foreground Service Manager regardless of notification permission state. Gating these calls together creates a crash path instead of preventing one.

What FCM actually costs

FCM message delivery is free, but nearby Firebase services can create a bill through automation and storage.

FCM itself carries no per-message fee on either Firebase plan. The official pricing page lists Cloud Messaging as a no-cost feature under both Spark and Blaze tiers.

Charges appear in adjacent services. Cloud Functions, which you would use for automated server-side notification triggers, requires the Blaze plan and an attached payment method. Firestore, often used for storing device tokens and notification metadata, has its own usage-based pricing on Blaze.

Set a budget alert in the Google Cloud Console if you activate Blaze. The alert can catch unexpected Firestore or Cloud Functions charges before they accumulate.

Start on Spark. You can send notifications from the Firebase Console or call the FCM HTTP v1 API from a simple external script. Move to Blaze only when you need Cloud Functions for automated triggers.

FCM also has quota controls that exist as abuse prevention. Exceeding them does not generate charges. These quotas limit delivery rate and have no billing threshold function.

Notification strategy that keeps users engaged

Reliable delivery is only half the job. The messages themselves determine whether users keep notifications turned on.

Use channels as a retention tool

Channels let users tune categories without disabling everything, which helps you keep more opt-ins.

Create distinct channels by notification type, such as "New messages" and "Activity updates." Users who find one category annoying can mute just that channel instead of disabling everything.

The official Android design guidelines recommend providing in-app notification settings so users can update preferences without hunting through system settings.

Content and frequency principles

Send fewer low-value alerts, and make the important ones easier to understand.

  • Match importance level to actual urgency. Do not disguise routine updates as urgent.
  • Use setOnlyAlertOnce() for notifications that update in place, such as progress tracking.
  • Use MessagingStyle for chat apps. Append new messages to an existing notification rather than creating a new one each time.
  • Write notification copy around the value to the user, not the feature generating it.

Start conservative with frequency. Recovering from a wave of uninstalls caused by over-sending is much harder than gradually increasing volume once you have signal on what users value.

Testing and debugging without a backend

Validate the full notification path before you build server automation. Start with a console send, then test one API-level payload. Finish with on-device checks for channels and token changes.

  1. Firebase Console Notifications Composer: can be used to test FCM messaging without writing server code, as described in the FCM documentation.
  2. Postman: test specific payload structures against the FCM HTTP v1 API endpoint. Authenticate with OAuth2. Deprecated server keys no longer apply.

One timing detail from the official FCM docs: if your app creates its first notification channel while running in the background, Android will not display the notification until the next app open.

Token refresh is a common debugging issue for solo developers. FCM tokens can be rotated by the system. Log the token in your FirebaseMessagingService.onNewToken() callback and update your server or test tool each time.

For delivery analytics and segmentation, you have two directions. Stay with the Firebase Console for FCM message reporting and delivery metrics at no cost. Or add a managed service for a dashboard, open rate tracking, and A/B testing. That adds a dependency, so weigh it against the extra work you are willing to own.

A working Android push setup comes down to four things: correct FCM configuration, permission timing that respects the user, compliance with current Android behavior changes, and a cost model that stays simple until you need more. If you are ready to test your build, use the FCM client guide as your next step and verify the full path on a real device today.