Complete Kotlin mapping of Apple's UserNotifications framework via JNI. Schedule local notifications with action buttons, text input, sounds, badges, and interruption levels.
Requires a packaged app
macOS notifications require a bundle identifier. Use ./gradlew runDistributable or ./gradlew runGraalvmNative — notifications are disabled when running via ./gradlew run.
importio.github.kdroidfilter.nucleus.notification.*// 1. Request authorizationNotificationCenter.requestAuthorization(setOf(AuthorizationOption.ALERT,AuthorizationOption.SOUND,AuthorizationOption.BADGE)){granted,error->if(granted){// 2. Send a notificationNotificationCenter.add(NotificationRequest(identifier="greeting",content=NotificationContent(title="Hello",body="Welcome to Nucleus!",sound=NotificationSound.Default,),trigger=NotificationTrigger.TimeInterval(interval=5.0),))}}
Async callbacks run on background threads
Completion callbacks (requestAuthorization, add, getNotificationSettings, etc.) are invoked on macOS's internal dispatch thread. Use SwingUtilities.invokeLater if you need to update Compose state from these callbacks. Delegate methods (willPresent, didReceive, openSettings) are automatically dispatched to the EDT by the library.
Set a NotificationCenterDelegate for foreground presentation and action callbacks. Pass null to remove. Without a delegate, notifications show banner + sound + list by default.
Implement this interface to control foreground notification display and handle user interactions.
Thread safety
All delegate methods are automatically dispatched to the AWT Event Dispatch Thread by the library. You can safely update Compose state (mutableStateOf, SnapshotStateList, etc.) directly in your delegate implementation without manual thread marshalling.
willPresent runs via invokeAndWait (synchronous, must return quickly). didReceive and openSettings run via invokeLater (asynchronous).
NotificationCenter.setDelegate(object:NotificationCenterDelegate{overridefunwillPresent(notification:DeliveredNotification):Set<PresentationOption>{// Return which presentation to use when app is in foregroundreturnsetOf(PresentationOption.BANNER,PresentationOption.SOUND)}overridefundidReceive(response:NotificationResponse){when(response.actionIdentifier){"reply"->println("User replied: ${response.userText}")"archive"->println("Archived")NotificationAction.DEFAULT_ACTION_IDENTIFIER->println("Tapped notification")NotificationAction.DISMISS_ACTION_IDENTIFIER->println("Dismissed")}}overridefunopenSettings(notification:DeliveredNotification?){// Optional: user tapped notification settings button}})
Method
Returns
Description
willPresent(notification)
Set<PresentationOption>
Called when notification arrives while app is foreground. Return empty set to suppress.
didReceive(response)
Unit
Called when user taps the notification or an action button.
openSettings(notification?)
Unit
Called when user taps the settings button. Optional, default no-op.
// Fire after 30 secondsNotificationTrigger.TimeInterval(interval=30.0)// Fire every day at 9:00NotificationTrigger.Calendar(dateComponents=DateComponents(hour=9,minute=0),repeats=true,)
Subclass
Properties
Description
TimeInterval
interval: Double, repeats: Boolean
Fire after N seconds. Repeating requires interval >= 60.
// Simple buttonNotificationAction(identifier="archive",title="Archive",options=setOf(ActionOption.DESTRUCTIVE),)// Text input buttonTextInputNotificationAction(identifier="reply",title="Reply",options=setOf(ActionOption.FOREGROUND),textInputButtonTitle="Send",textInputPlaceholder="Type your reply...",)
Critical alerts — requirescom.apple.developer.usernotifications.critical-alerts entitlement. Including this option without the entitlement causes the entire authorization request to fail silently.
PROVIDES_APP_NOTIFICATION_SETTINGS
App provides custom settings
PROVISIONAL
Provisional authorization (no prompt)
TIME_SENSITIVE
Time-sensitive notifications (macOS 12+) — may require entitlement on some macOS versions
// Register categories with action buttonsvalreplyAction=TextInputNotificationAction(identifier="reply",title="Reply",options=setOf(ActionOption.FOREGROUND),textInputButtonTitle="Send",textInputPlaceholder="Type your reply...",)valmarkReadAction=NotificationAction(identifier="mark-read",title="Mark as Read")valdeleteAction=NotificationAction(identifier="delete",title="Delete",options=setOf(ActionOption.DESTRUCTIVE),)NotificationCenter.setNotificationCategories(setOf(NotificationCategory(identifier="message",actions=listOf(replyAction,markReadAction,deleteAction),options=setOf(CategoryOption.CUSTOM_DISMISS_ACTION),)))// Set delegate to handle action callbacksNotificationCenter.setDelegate(object:NotificationCenterDelegate{overridefunwillPresent(notification:DeliveredNotification)=setOf(PresentationOption.BANNER,PresentationOption.SOUND,PresentationOption.LIST)overridefundidReceive(response:NotificationResponse){when(response.actionIdentifier){"reply"->sendMessage(response.userText?:"")"mark-read"->markAsRead(response.notification.identifier)"delete"->deleteMessage(response.notification.identifier)NotificationAction.DEFAULT_ACTION_IDENTIFIER->openConversation()}}})// Send a notification with actionsNotificationCenter.add(NotificationRequest(identifier="msg-42",content=NotificationContent(title="Alice",subtitle="Project Nucleus",body="Hey! Have you seen the latest build?",sound=NotificationSound.Default,categoryIdentifier="message",threadIdentifier="conversation-alice",),trigger=NotificationTrigger.TimeInterval(interval=1.0),)){error->if(error!=null)println("Failed: $error")}
Critical alerts bypass Do Not Disturb and Focus modes. They require:
An Apple-issued entitlement (com.apple.developer.usernotifications.critical-alerts) — request from Apple
The entitlement declared in your entitlements.plist
AuthorizationOption.CRITICAL_ALERT in requestAuthorization
Do NOT include CRITICAL_ALERT without the entitlement
If you pass AuthorizationOption.CRITICAL_ALERT in requestAuthorization without the Apple-issued entitlement, the entire authorization request fails — macOS returns granted = false with error "Notifications are not allowed for this application" and the permission dialog is never shown. The same applies to TIME_SENSITIVE on some macOS versions.
Only use ALERT, SOUND, and BADGE unless you have obtained the corresponding entitlement from Apple:
// Safe default — works without special entitlementsNotificationCenter.requestAuthorization(setOf(AuthorizationOption.ALERT,AuthorizationOption.SOUND,AuthorizationOption.BADGE)){granted,error->...}
// Only use this if you have the com.apple.developer.usernotifications.critical-alerts entitlementNotificationCenter.requestAuthorization(setOf(AuthorizationOption.ALERT,AuthorizationOption.SOUND,AuthorizationOption.CRITICAL_ALERT)){granted,_->if(granted){NotificationCenter.add(NotificationRequest(identifier="critical-1",content=NotificationContent(title="System Alert",body="Immediate attention required",sound=NotificationSound.DefaultCritical,interruptionLevel=InterruptionLevel.CRITICAL,),trigger=NotificationTrigger.TimeInterval(interval=1.0),))}}
Reachability metadata is included in the JAR at META-INF/native-image/io.github.kdroidfilter/nucleus.notification-macos/reachability-metadata.json. No additional configuration needed.