Skip to content
Push Notifications: iOS and Android Implementation Guide

Push Notifications: iOS and Android Implementation Guide

DodaTech Updated Jun 20, 2026 8 min read

Push notifications are messages sent from a server to a mobile device that appear outside the app — using Apple Push Notification service (APNs) for iOS and Firebase Cloud Messaging (FCM) for Android to deliver time-sensitive updates, alerts, and data payloads.

What You’ll Learn

You’ll implement push notifications with APNs and FCM, manage device tokens, design notification payloads, handle foreground and background delivery, create notification channels, build rich notifications with images and actions, and implement deep linking from notifications.

Why Push Notifications Matter

Push notifications are the primary channel for re-engaging users. Doda Browser uses notifications to alert users about downloads completing. Durga Antivirus Pro sends real-time threat alerts. Push notifications increase retention by up to 3x — users who enable notifications return to the app significantly more often than those who don’t.

Push Notifications Learning Path

    flowchart LR
  A[Mobile Development Overview] --> B[Android / iOS Development]
  B --> C[Mobile Security]
  C --> D[Push Notifications]
  D --> E[App Store Deployment]
  D:::current
  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
  
Prerequisites: A physical iOS or Android device (push notifications don’t work well in simulators), an Apple Developer account (for APNs), and a Firebase project (for FCM).

How Push Notifications Work

    flowchart LR
    App[Your App] -->|1. Register for notifications| OS[OS Push Service]
    OS -->|2. Returns device token| App
    App -->|3. Send token to your server| Server[Your Backend]
    Server -->|4. Send notification request| PushService[APNs / FCM]
    PushService -->|5. Deliver notification| Device[User Device]
    Device -->|6. App handles tap| App
  

FCM (Firebase Cloud Messaging) — Android

Step 1: Add Firebase to your project

// build.gradle.kts (project level)
plugins {
    id("com.google.gms.google-services") version "4.4.1" apply false
}

// build.gradle.kts (app level)
plugins {
    id("com.google.gms.google-services")
}

dependencies {
    implementation("com.google.firebase:firebase-messaging:24.0.0")
}

Step 2: Create a service to handle messages

import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

class DodaTechMessagingService : FirebaseMessagingService() {

    // Called when a new token is generated
    override fun onNewToken(token: String) {
        super.onNewToken(token)
        // Send token to your backend server
        sendTokenToServer(token)
    }

    // Called when a message is received
    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)

        // Check message type
        message.notification?.let {
            // Display notification
            showNotification(it.title ?: "", it.body ?: "")
        }

        message.data.let { data ->
            // Handle data payload (silent notification)
            handleDataPayload(data)
        }
    }

    private fun showNotification(title: String, body: String) {
        val channelId = "general_notifications"
        val notificationManager = getSystemService(NotificationManager::class.java)

        // Create channel (Android 8+)
        val channel = NotificationChannel(
            channelId,
            "General Notifications",
            NotificationManager.IMPORTANCE_DEFAULT
        )
        notificationManager.createNotificationChannel(channel)

        // Build notification
        val notification = NotificationCompat.Builder(this, channelId)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(title)
            .setContentText(body)
            .setAutoCancel(true)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .build()

        notificationManager.notify(System.currentTimeMillis().toInt(), notification)
    }
}

Step 3: Request permission (Android 13+)

// In your Activity
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
        != PackageManager.PERMISSION_GRANTED
    ) {
        requestPermissions(
            arrayOf(Manifest.permission.POST_NOTIFICATIONS),
            NOTIFICATION_PERMISSION_CODE
        )
    }
}

// Get device token
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
    if (task.isSuccessful) {
        val token = task.result
        Log.d("FCM", "Token: $token")
        // Send to server
    }
}

Step 4: Send a notification from your server

import requests

def send_fcm_notification(token, title, body):
    url = "https://fcm.googleapis.com/fcm/send"
    headers = {
        "Authorization": "key=YOUR_SERVER_KEY",
        "Content-Type": "application/json"
    }
    payload = {
        "to": token,
        "notification": {
            "title": title,
            "body": body,
            "sound": "default"
        },
        "data": {
            "type": "new_message",
            "conversation_id": "123"
        }
    }
    response = requests.post(url, json=payload, headers=headers)
    print(response.json())

Expected response:

{"multicast_id": 123456789, "success": 1, "failure": 0, "canonical_ids": 0}

APNs (Apple Push Notification Service) — iOS

Step 1: Enable Push Notifications in Xcode

  • Go to Target > Signing & Capabilities > + Capability > Push Notifications
  • Enable Remote notifications in Background Modes

Step 2: Register for notifications

import UIKit
import UserNotifications

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // Request permission
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if granted {
                DispatchQueue.main.async {
                    application.registerForRemoteNotifications()
                }
            }
        }

        return true
    }

    // Called when device token is received
    func application(_ application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let tokenParts = deviceToken.map { String(format: "%02.2hhx", $0) }
        let token = tokenParts.joined()
        print("APNs Token: \(token)")
        // Send to your backend server
        sendTokenToServer(token)
    }

    func application(_ application: UIApplication,
                     didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Failed to register: \(error)")
    }
}

Step 3: Handle incoming notifications

class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
    // Called when notification is delivered while app is in foreground
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

        let userInfo = notification.request.content.userInfo
        print("Received notification: \(userInfo)")

        // Show banner even when app is in foreground
        completionHandler([.banner, .sound, .badge])
    }

    // Called when user taps on notification
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {

        let userInfo = response.notification.request.content.userInfo
        handleNotificationTap(userInfo)
        completionHandler()
    }

    private func handleNotificationTap(_ userInfo: [AnyHashable: Any]) {
        guard let type = userInfo["type"] as? String else { return }

        switch type {
        case "new_message":
            // Navigate to conversation screen
            navigateToConversation(id: userInfo["conversation_id"] as? String ?? "")
        case "alert":
            // Navigate to alert details
            navigateToAlert(id: userInfo["alert_id"] as? String ?? "")
        default:
            break
        }
    }
}

Notification Payloads

FCM Payload

{
  "to": "device_token_here",
  "notification": {
    "title": "New Message",
    "body": "Alice: Are you free tomorrow?",
    "image": "https://example.com/image.jpg"
  },
  "data": {
    "type": "new_message",
    "conversation_id": "conv_123",
    "sender_id": "user_456"
  },
  "android": {
    "priority": "high",
    "notification": {
      "channel_id": "messages",
      "click_action": "OPEN_CONVERSATION"
    }
  }
}

APNs Payload

{
  "aps": {
    "alert": {
      "title": "New Message",
      "body": "Alice: Are you free tomorrow?"
    },
    "sound": "default",
    "badge": 5,
    "category": "message"
  },
  "type": "new_message",
  "conversation_id": "conv_123"
}

Rich Notifications

Notifications with images, actions, and custom UI.

Android: Notification with image

val bigPictureStyle = NotificationCompat.BigPictureStyle()
    .bigPicture(bitmap)
    .setSummaryText("Tap to view photo")

val notification = NotificationCompat.Builder(this, channelId)
    .setSmallIcon(R.drawable.ic_notification)
    .setContentTitle("Photo received")
    .setContentText("Alice sent you a photo")
    .setStyle(bigPictureStyle)
    .addAction(R.drawable.ic_reply, "Reply", replyPendingIntent)
    .build()

iOS: Notification with image and actions

// Register notification actions
let replyAction = UNNotificationAction(identifier: "REPLY", title: "Reply", options: .authenticationRequired)
let dismissAction = UNNotificationAction(identifier: "DISMISS", title: "Dismiss", options: .destructive)
let category = UNNotificationCategory(identifier: "message", actions: [replyAction, dismissAction], intentIdentifiers: [])

UNUserNotificationCenter.current().setNotificationCategories([category])

Deep Linking from Notifications

Deep linking navigates users to specific content when they tap a notification.

Android Deep Linking

<!-- AndroidManifest.xml -->
<activity android:name=".ConversationActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="dodatech" android:host="conversation" />
    </intent-filter>
</activity>
// Navigate to deep link
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("dodatech://conversation/conv_123"))
startActivity(intent)

iOS Deep Linking

// AppDelegate.swift — Handle universal link
func application(_ application: UIApplication,
                 continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else { return false }

    handleDeepLink(url: url)
    return true
}

Common Push Notification Errors

1. Missing or Invalid Device Token

If the device token is nil or changes unexpectedly, notifications won’t deliver. Always send the latest token to your server, and handle token refresh callbacks.

2. Not Requesting Permission Properly

iOS and Android 13+ require explicit permission. If you don’t request it, notifications are blocked silently. Check permission status before sending.

3. Foreground Notifications Not Showing

On both platforms, notifications don’t display automatically when the app is in the foreground. You must handle this in code (see willPresent delegate on iOS, onMessageReceived on Android).

4. Payload Size Limits

APNs limit payloads to 4KB. FCM allows up to 4KB for notifications and 2KB for data payloads. Keep payloads small.

5. Not Handling Notification Tapping

If a user taps a notification and nothing happens, they’ll uninstall. Always implement the tap handler to navigate to relevant content.

6. Sending Notifications to Stale Tokens

When a user uninstalls and reinstalls, their token changes. FCM returns NotRegistered error for stale tokens. Remove them from your database.

7. Notification Channel Mismatch (Android)

If you send a notification with a channel ID that doesn’t exist, the notification is dropped. Create all channels before the first notification.

Practice Questions

1. What’s the difference between FCM and APNs?

FCM (Firebase) is Google’s cross-platform push service for Android and iOS. APNs is Apple’s native service for iOS. FCM can deliver to both platforms, while APNs only works on Apple devices.

2. How do you handle a notification when the app is in the foreground?

On iOS, implement userNotificationCenter(_:willPresent:) and specify presentation options. On Android, handle it in onMessageReceived() and build a notification.

3. What is a device token and why does it change?

A device token is a unique identifier the push service assigns to your app on a specific device. It changes when the user reinstalls the app, restores from backup, or on iOS when they reset their device.

4. How do you implement deep linking from a notification?

Include a data payload with navigation parameters. When the user taps the notification, read the data payload and navigate to the appropriate screen using deep links or intent extras.

5. Challenge: Build a notification system for a chat app.

Design a push notification system that: shows the message preview, includes a “Reply” action button, groups notifications by conversation, and deep links to the specific conversation on tap. Implement it for both iOS and Android.

FAQ

Should I use FCM or APNs for iOS?
FCM works on iOS too and provides a unified API. However, FCM on iOS still relies on APNs under the hood. For a cross-platform app, use FCM. For iOS-only, APNs is simpler and slightly more reliable.
What’s the maximum notification payload size?
APNs: 4KB. FCM: 4KB for notifications, 2KB for data payloads. Exceeding these limits causes delivery failures.
How often do device tokens change?
iOS tokens change on app reinstall and occasionally on OS updates. Android (FCM) tokens change on app reinstall, data wipe, or token refresh (every ~6 months). Always handle onNewToken callbacks.
Can I send notifications without a server?
Yes, using Firebase Console for manual sends, or cloud functions (Firebase Functions) for automated sends. A dedicated backend is recommended for production.

Try It Yourself

Implement push notifications in a test app:

  1. Set up FCM for Android (or APNs for iOS)
  2. Request notification permission
  3. Get the device token and log it
  4. Send a test notification from Firebase Console
  5. Handle the notification tap to navigate to a specific screen
  6. Add a notification channel/settings screen

What’s Next

Push notifications are one of the most effective user engagement tools. Start with a simple notification for a key user action (like “Download complete” in DodaZIP), then expand into rich notifications with images, actions, and deep linking as your notification strategy matures.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro