KMPNotifier - Kotlin Multiplatform Notification



Simple and easy to use Kotlin Multiplatform Notification library: local notifications targeting android, iOS, desktop and web (js and wasm), and push notifications (Firebase Cloud Messaging) targeting android and iOS.
This library is used in FindTravelNow production KMP project.
You can check out Documentation for full library api information.

Related Blog Posts
KMPNotifier Update: Web, Desktop, and New Features for Kotlin Multiplatform Notifications
How to implement Push Notifications in Kotlin Multiplatform
Features
- 🔔 Local notifications (android, ios, desktop, js and wasm)
- ⏰ Scheduled notifications (android and ios)
- 🎬 Action buttons, with optional inline text input (android and ios)
- 🖼️ Rich notifications with images, from URL or local file (android and ios)
- 🔗 Click & payload delivery for deep linking
- ☁️ Push notifications (Firebase Cloud Messaging) (android and ios only)
- 📱 Multiplatform (android, iOS, desktop and web (js and wasm))
- 📦 Modular: use local notifications without pulling in Firebase
Feature / platform support
⛔️ shows now = the call is accepted but the notification is shown immediately (the schedule is ignored). no-op mock = API exists and compiles, token is null, calls do nothing — so shared code needs no expect/actual.
Modules
Since 2.0.0 the library is split into focused modules:
Upgrading from 1.x? Your code keeps working — see MIGRATION.md for the
deprecation mapping and CHANGELOG.md for what changed.
Installation
For push notifications you need the basic Firebase setup following the official guideline (initializing project in Firebase, adding google-services.json to android, GoogleService-Info.plist to iOS). Local-only usage needs no Firebase setup at all.
Minimum Requirements
- Kotlin:
2.4.0+ (the library is built with Kotlin 2.4 and consumes Firebase via Swift Package Manager)
- Android:
minSdkVersion 23
- iOS:
iOS 16.0 for push (kmpnotifier-push-firebase); local-only modules work on lower targets
Gradle Setup
KMPNotifier is available on Maven Central. In your root project build.gradle.kts file (or settings.gradle file) add mavenCentral() to repositories. If you use push notifications, also add the google-services plugin.
repositories {
mavenCentral()
}
Then add the dependency in your shared module. Latest version:
. In the iOS framework part, export the modules as well.
sourceSets {
commonMain.dependencies {
api("io.github.mirzemehdi:kmpnotifier-local:<version>")
api("io.github.mirzemehdi:kmpnotifier-push-firebase:<version>")
}
}
listOf(iosX64(),iosArm64(),iosSimulatorArm64()).forEach { iosTarget ->
iosTarget.binaries.framework {
export("io.github.mirzemehdi:kmpnotifier-core:<version>")
export("io.github.mirzemehdi:kmpnotifier-local:<version>")
export("io.github.mirzemehdi:kmpnotifier-push-firebase:<version>")
...
}
}
If you use push notifications, apply the google-services plugin in your androidApp build.gradle.kts file:
plugins {
id("com.android.application")
id("com.google.gms.google-services")
}
Platform Setup
On application start you initialize the library once with the platform configuration and the capabilities (extensions) you want:
KMPNotifier.initialize(configuration, LocalNotifications)
KMPNotifier.initialize(configuration, FirebasePush)
Note: on iOS, initialize must be called from the main thread — the notification delegate is installed during init and is required to receive cold-start clicks. Calling initialize again is a no-op for the configuration, but it does install any extension not yet installed.
Desktop
Desktop Setup
You need to put notification icon into resources/common folder. For more information:
Compose Desktop Resources
Web
Web Setup (Js and Wasm)
On application start initialize it using Web configuration
fun main() {
KMPNotifier.initialize(
NotificationPlatformConfiguration.Web(
askNotificationPermissionOnStart = true,
notificationIconPath = null
),
LocalNotifications,
)
}
Usage
You access the two notifiers through the KMPNotifier facade:
KMPNotifier.localNotifier — local notifications (all platforms).
KMPNotifier.firebasePushNotifier — push token / topic management (real on android/ios, no-op mock elsewhere).
All snippets below need the library to be initialized first (see Platform Setup). Accessing a notifier before initialize throws IllegalStateException. Use KMPNotifier.isInitialized to guard if needed.
Local Notifications
Local notifications are supported on all targets. The richest entry point is the notify { } builder DSL.
Send a notification (builder DSL)
KMPNotifier.localNotifier.notify {
id = Random.nextInt(0, Int.MAX_VALUE)
title = "Title from KMPNotifier"
body = "Body message from KMPNotifier"
payloadData = mapOf(
Notifier.KEY_URL to "https://github.com/mirzemehdi/KMPNotifier/",
"extraKey" to "randomValue"
)
image = NotificationImage.Url("https://github.com/user-attachments/assets/a0f38159-b31d-4a47-97a7-cc230e15d30b")
}
Every builder property:
There is also a payload { } sub-DSL if you prefer building the map inline:
KMPNotifier.localNotifier.notify {
title = "Order shipped"
body = "Tap to track"
payload {
put(Notifier.KEY_URL, "myapp://orders/42")
put("orderId", "42")
}
}
Send quickly (without the DSL)
val notifier = KMPNotifier.localNotifier
val id: Int = notifier.notify(title = "Hi", body = "Quick notification")
notifier.notify(id = 100, title = "Hi", body = "Quick notification")
Schedule a notification
Set scheduledAt to the epoch milliseconds at which the notification should fire. Supported on Android (via AlarmManager) and iOS (via UNTimeIntervalNotificationTrigger). On desktop and web the value is ignored and the notification shows immediately.
KMPNotifier.localNotifier.notify {
id = 777
title = "Reminder"
body = "Stand up and stretch 🧘"
scheduledAt = Clock.System.now().toEpochMilliseconds() + 60_000
}
Cancel a scheduled (not-yet-fired) notification the same way you remove a shown one — remove(id) cancels the pending alarm too:
KMPNotifier.localNotifier.remove(777)
Android exact alarms: the library uses an exact alarm and falls back to an inexact one if the OS denies exact scheduling. There is no repeating-notification API.
Action buttons (and inline text input)
Add NotificationActions in the builder. Supported on Android and iOS.
KMPNotifier.localNotifier.notify {
id = 200
title = "New message"
body = "Alex: are we still on for lunch?"
actions = listOf(
NotificationAction(id = "OPEN", title = "Open"),
NotificationAction(
id = "REPLY",
title = "Reply",
allowsTextInput = true,
inputLabel = "Type your reply…"
),
)
}
NotificationAction:
When the user taps an action it is delivered to onAction (see below). If the action allows text input, the entered text arrives in the payload under the key "remote_input".
Notification images
image = NotificationImage.Url("https://example.com/picture.png")
image = NotificationImage.File("/path/to/local/picture.png")
Rendered as a big picture on Android and as an attachment on iOS. Only Url and File exist (no resource variant).
Remove notifications
val notifier = KMPNotifier.localNotifier
notifier.remove(notificationId)
notifier.removeAll()
Notification click & action listener
Clicks and action buttons are shared events — they fire for both local and push notifications. Register a KMPNotifier.Listener:
Both callbacks have default empty bodies, so override only what you need. Manage listeners with addListener / removeListener / setListener(null) (the last removes all).
Push Notifications
Push notifications are delivered on Android and iOS via the kmpnotifier-push-firebase module. The module compiles on every target — on desktop and web the notifier is a no-op mock (token is null), so shared code needs no expect/actual.
Listen for push events
Push-specific events (token updates, push payloads) use PushListener. Register it via KMPNotifier.addPushListener:
All callbacks default to empty, so override only the ones you use. Manage with addPushListener / removePushListener / setPushListener(null).
To receive the payload data correctly you must call the platform hooks below.
Android
Call KMPNotifier.onCreateOrOnNewIntent(intent) in your launcher Activity's onCreate and onNewIntent:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
KMPNotifier.onCreateOrOnNewIntent(intent)
...
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
KMPNotifier.onCreateOrOnNewIntent(intent)
}
iOS
Call KMPNotifier.onApplicationDidReceiveRemoteNotification(userInfo:) in your app's didReceiveRemoteNotification:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult {
KMPNotifier.shared.onApplicationDidReceiveRemoteNotification(userInfo: userInfo)
return UIBackgroundFetchResult.newData
}
Token & topic management
KMPNotifier.firebasePushNotifier.getToken()
KMPNotifier.firebasePushNotifier.deleteMyToken()
KMPNotifier.firebasePushNotifier.subscribeToTopic("new_users")
KMPNotifier.firebasePushNotifier.unSubscribeFromTopic("new_users")
Permissions
KMPNotifier.permissionUtil exposes callback-based notification-permission helpers on every platform:
val permissionUtil = KMPNotifier.permissionUtil
permissionUtil.hasNotificationPermission { granted -> }
permissionUtil.askNotificationPermission { granted -> }
On iOS and web the configuration's askNotificationPermissionOnStart flag asks for permission automatically at init.
Asking notification permission (Android 13+)
POST_NOTIFICATIONS (API 33+) must be requested from an Activity. The library provides a ComponentActivity extension that wires ActivityResultContracts.RequestPermission for you. On API < 33 it reports true immediately.
class MainActivity : ComponentActivity() {
private val permissionUtil by permissionUtil()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
permissionUtil.askNotificationPermission { granted ->
}
}
}
Notification channel & sound
For more detail and examples:
Logging
To see the library's internal logs, set a logger:
KMPNotifier.setLogger { message ->
println(message)
}
Migrating from 1.x
The old NotifierManager API (from the kmpnotifier artifact) keeps working in 2.x —
it is deprecated and forwards to the new API. See MIGRATION.md.