Push Notifications
AFCore integrates with Firebase Cloud Messaging (FCM) and iOS APNs to deliver push notifications.
This guide shows how to configure your app, request permissions, manage tokens, and interact with the unified AFCore.messaging() API.
You’ll need a Firebase project and
google-services.json(Android) and APNs/Firebase setup (iOS).
Android — Setup
-
Firebase Console
- Create or choose a project → add Android app (package matches your app).
- Download
google-services.jsonintoapp/. - Enable Cloud Messaging.
-
Gradle
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.gms.google-services") // 👈
}
dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.1.2"))
implementation("com.google.firebase:firebase-messaging")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
}
- Notification Channel (Android 8+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"afcore_default",
"AFCore Notifications",
NotificationManager.IMPORTANCE_DEFAULT
)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
-
SDK Service
AFCore already declares its ownFirebaseMessagingServicein the library’sAndroidManifest.xml.
Do not add another service to your app. -
(Optional) Tell AFCore about your desired presentation
AFCore.messaging().setNotificationOptions(
NotificationOptions(androidChannelId = "afcore_default")
)
iOS — Setup
On iOS you must bridge APNs/FCM events to AFCore.
- Configure Firebase in your AppDelegate with
FirebaseApp.configure(). - Request notification authorization with
UNUserNotificationCenter. - Register for remote notifications with
UIApplication.shared.registerForRemoteNotifications(). - Forward APNs tokens to
Messaging.messaging().apnsToken. - Forward FCM tokens and foreground messages into AFCore using
AFMessagingHooks(see below).
Permission & token flow (explicit)
ensurePermissionAndRegister(deviceId)is deprecated.
Use AFPermissions to request POST_NOTIFICATIONS (Android 13+) / notification authorization (iOS), then callupdateToken(deviceId).updateTokenensures a device token exists and updates it in your backend.getToken()reads the token from your backend, anddeleteToken(deviceId)removes the backend association.
Android (Kotlin)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
fun preparePush(context: Context) {
val deviceId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
)
scope.launch {
// 1) Ask/verify POST_NOTIFICATIONS (Android 13+). requestPermission() returns Unit.
var granted = AFPermissions.isPermissionGranted(Permission.NOTIFICATIONS)
if (!granted) {
AFPermissions.requestPermission(Permission.NOTIFICATIONS) // Unit
granted = AFPermissions.isPermissionGranted(Permission.NOTIFICATIONS) // re-check
}
if (!granted) {
// User denied; consider showing a rationale or deep-link to settings
return@launch
}
// 2) Register or refresh token in your backend (idempotent)
val ok = AFCore.messaging().updateToken(deviceId)
if (!ok) {
// handle failure (retry/backoff)
}
}
}
iOS (Swift)
func preparePush() {
Task {
let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
// 1) Ask/verify permission via AFPermissions. requestPermission() returns Void.
var granted = (try? await AFPermissions.shared.isPermissionGranted(permission: .notifications)) == true
if !granted {
await AFPermissions.shared.requestPermission(permission: .notifications) // Void
granted = (try? await AFPermissions.shared.isPermissionGranted(permission: .notifications)) == true // re-check
}
guard granted else { return }
// 2) Register or refresh token in your backend (idempotent)
let ok = (try? await AFCore.shared.messaging().updateToken(deviceId: deviceId)) ?? false
if !ok {
// handle failure (retry/backoff)
}
}
}
Token management
Get token (from backend)
val token = AFCore.messaging().getToken()
let token = try await AFCore.shared.messaging().getToken()
Update/refresh token on backend
Useful on first install, after FCM/APNs rotation, or app upgrades.
val ok = AFCore.messaging().updateToken(deviceId)
let ok = try await AFCore.shared.messaging().updateToken(deviceId: deviceId)
Delete token (backend)
Call on logout/unlink to stop notifications for this device.
val ok = AFCore.messaging().deleteToken(deviceId)
let ok = try await AFCore.shared.messaging().deleteToken(deviceId: deviceId)
Topics
Subscribe/unsubscribe to topics for cohort targeting (marketing, beta, region).
AFCore.messaging().subscribe("beta")
AFCore.messaging().unsubscribe("beta")
let ok = try await AFCore.shared.messaging().subscribe(topic: "beta")
let removed = try await AFCore.shared.messaging().unsubscribe(topic: "beta")
Foreground messages
Collect push data while app is active.
private val pushScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
fun observeMessages() {
pushScope.launch {
AFCore.messaging().messages().collect { msg: IncomingMessage ->
// msg.title, msg.body, msg.data (map), msg.topic, etc.
// Show in-app banner or route the user
}
}
}
Task.detached {
for await msg in AFCore.shared.messaging().messages() {
// msg.title, msg.body, msg.data, msg.deeplink, msg.icon
}
}
Cancel the scope/task when appropriate (e.g., onStop).
iOS bridging
Use AFMessagingHooks in AppDelegate to connect APNs/FCM with AFCore:
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
AFMessagingHooks.setFcmToken(token: fcmToken)
}
Forward foreground messages:
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
AFMessagingHooks.onForegroundMessage(
title: notification.request.content.title,
body: notification.request.content.body,
deeplink: notification.request.content.userInfo["deeplink"] as? String,
icon: notification.request.content.userInfo["icon"] as? String,
data: notification.request.content.userInfo as? [String: String]
)
completionHandler([.banner, .list, .sound])
}
Best practices
- Ask early, not immediately: Request notifications at a relevant moment so users understand the value.
- Idempotent backend:
updateToken(deviceId)should be safe to call on every cold start. - Handle denial: gracefully continue if permission denied; offer deep‑link to settings.
- Privacy: document device IDs and tokens in your privacy policy.
- Environments: QA vs Prod Firebase projects must not share credentials.
- Logout: call
deleteToken(deviceId)when logging out to stop push for that device. - Rotation aware: call
updateTokenif FCM/APNs token changes (AFCore emits hooks; also consider calling on app start).
Troubleshooting
- No notifications: verify Firebase project,
google-services.json, APNs certificates, and bundle IDs. - Token is null: Play Services missing/disabled (Android) or APNs not delivered (iOS). Retry later.
- Background delivery issues: check Doze (Android), OEM restrictions, iOS background modes, and channel importance.
- Multiple devices: register distinct
deviceIdvalues per device. - Duplicate service warning (Android): do not add your own FirebaseMessagingService, AFCore provides one.