Swift Notifications
One API. Three Platforms. Zero Native Setup.
A unified Flutter plugin for rich push and local notifications on Android, iOS, and macOS with no native code required.
🌟 Features
- ✅ Unified API - Same code works on Android, iOS, and macOS
- ✅ Rich Notifications - Support for images and action buttons
- ✅ Zero Native Setup - No manifest edits, no plist configuration, no native code
- ✅ Automatic Setup - Permissions, channels, categories handled automatically
- ✅ Works Everywhere - Foreground, background, and terminated app states
- ✅ Graceful Fallbacks - Falls back to text notifications if images fail
📦 Installation
Add this to your package's pubspec.yaml file:
dependencies:
swift_notifications: ^0.0.1
Then run:
flutter pub get
🚀 Quick Start
1. Initialize the Plugin
import 'package:swift_notifications/swift_notifications.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final notifications = SwiftNotifications();
await notifications.initialize();
await notifications.requestPermission();
runApp(MyApp());
}
2. Show a Simple Notification
await notifications.showSimpleNotification(
id: 'notification_1',
title: 'Hello!',
body: 'This is a simple notification',
);
3. Show a Rich Notification with Image
await notifications.showImageNotification(
id: 'notification_2',
title: 'Check this out!',
body: 'This notification has an image',
imageUrl: 'https://example.com/image.jpg',
);
4. Show a Notification with Buttons
await notifications.showNotification(
NotificationRequest(
id: 'notification_3',
title: 'New Message',
body: 'You have a new message',
buttonsEnabled: true,
actions: [
NotificationAction(
id: 'reply',
title: 'Reply',
payload: {'action': 'reply'},
),
NotificationAction(
id: 'delete',
title: 'Delete',
isDestructive: true,
payload: {'action': 'delete'},
),
],
),
);
5. Handle Notification Responses
notifications.onNotificationResponse.listen((response) {
print('Notification ID: ${response.notificationId}');
print('Action ID: ${response.actionId}');
print('Payload: ${response.payload}');
if (response.actionId == 'reply') {
// Handle reply action
}
});
📚 API Reference
SwiftNotifications
Main class for managing notifications.
Methods
initialize()
Initialize the notification plugin. Call this once when your app starts.
await notifications.initialize();
requestPermission()
Request notification permissions from the user.
final status = await notifications.requestPermission();
// Returns: NotificationPermissionStatus.granted, .denied, etc.
checkPermission()
Check the current permission status without requesting.
final status = await notifications.checkPermission();
showNotification(NotificationRequest request)
Show a notification with full customization.
await notifications.showNotification(
NotificationRequest(
id: 'unique_id',
title: 'Title',
body: 'Body',
// ... see NotificationRequest for all options
),
);
showSimpleNotification({required String id, required String title, required String body, Map<String, dynamic>? payload})
Convenience method for simple text notifications.
await notifications.showSimpleNotification(
id: 'simple_1',
title: 'Hello',
body: 'World',
payload: {'key': 'value'},
);
showImageNotification({required String id, required String title, required String body, required String imageUrl, Map<String, dynamic>? payload})
Convenience method for notifications with images.
await notifications.showImageNotification(
id: 'image_1',
title: 'Image',
body: 'Notification',
imageUrl: 'https://example.com/image.jpg',
);
cancelNotification(String notificationId)
Cancel a specific notification.
await notifications.cancelNotification('notification_1');
cancelAllNotifications()
Cancel all notifications.
await notifications.cancelAllNotifications();
Streams
onNotificationResponse
Stream of notification responses (taps and button clicks).
notifications.onNotificationResponse.listen((response) {
// Handle notification response
});
NotificationRequest
Model for notification configuration.
NotificationRequest(
id: 'unique_id', // Required: Unique identifier
title: 'Title', // Required: Notification title
body: 'Body', // Required: Notification body
image: NotificationImage(...), // Optional: Image to display
imageEnabled: true, // Optional: Enable image feature
actions: [NotificationAction(...)], // Optional: Action buttons
buttonsEnabled: true, // Optional: Enable buttons feature
categoryId: 'custom_category', // Optional: Category identifier
payload: {'key': 'value'}, // Optional: Custom payload
sound: 'default', // Optional: Sound name
badge: 1, // Optional: Badge number (iOS/macOS)
priority: NotificationPriority.high, // Optional: Priority (Android)
showWhenForeground: true, // Optional: Show when app is in foreground
)
NotificationAction
Model for notification action buttons.
NotificationAction(
id: 'action_id', // Required: Unique action identifier
title: 'Action Title', // Required: Button label
payload: {'key': 'value'}, // Optional: Action payload
requiresAuthentication: false, // Optional: Require authentication
isDestructive: false, // Optional: Mark as destructive (red on iOS)
)
NotificationImage
Model for notification images.
NotificationImage(
url: 'https://example.com/image.jpg', // Required: Image URL
localPath: '/path/to/image.jpg', // Optional: Local file path
downloadFromUrl: true, // Optional: Download from URL
)
NotificationResponse
Model for notification responses.
NotificationResponse(
notificationId: 'notification_1', // The notification that was tapped
actionId: 'reply', // The action button clicked (null if body tapped)
payload: {'key': 'value'}, // Notification payload
actionPayload: {'action': 'reply'}, // Action payload (if action clicked)
)
🎯 Platform Support
| Feature | Android | iOS | macOS |
|---|---|---|---|
| Basic Notifications | ✅ | ✅ | ✅ |
| Images | ✅ | ✅ | ✅ |
| Buttons | ✅ | ✅ | ✅ |
| Categories | ✅ | ✅ | ✅ |
| Permissions | ✅ | ✅ | ✅ |
| Foreground Display | ✅ | ✅ | ✅ |
| Background Display | ✅ | ✅ | ✅ |
| Terminated Display | ✅ | ✅ | ✅ |
🔧 Platform-Specific Notes
Android
- Minimum SDK: 24 (Android 7.0)
- Permissions: Automatically handled. POST_NOTIFICATIONS permission is declared in the plugin.
- Channels: Automatically created. Default channel ID:
swift_notifications_default - Images: Downloaded and displayed using BigPictureStyle
- Buttons: Implemented using NotificationCompat actions
iOS
- Minimum Version: iOS 10.0
- Permissions: Requested automatically via UNUserNotificationCenter
- Categories: Automatically created and registered
- Images: Downloaded and attached as UNNotificationAttachment
- Buttons: Implemented using UNNotificationAction
macOS
- Minimum Version: macOS 10.14
- Permissions: Requested automatically via UNUserNotificationCenter
- Categories: Automatically created and registered
- Images: Downloaded and attached as UNNotificationAttachment
- Buttons: Implemented using UNNotificationAction
📡 Remote Push Notifications
Android (Firebase Cloud Messaging)
To use remote push notifications with Firebase, you need to create your own Firebase Messaging Service. The plugin provides a helper class NotificationBuilder to make this easy:
-
Add Firebase to your app:
dependencies: firebase_core: ^2.24.0 firebase_messaging: ^14.7.0 -
Add Firebase Messaging dependency in
android/app/build.gradle:dependencies { implementation 'com.google.firebase:firebase-messaging:23.4.0' } -
Create your Firebase Messaging Service using the helper class:
Create
android/app/src/main/kotlin/com/yourapp/YourFirebaseMessagingService.kt:package com.yourapp import android.util.Log import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.swiftflutter.swift_notifications.NotificationBuilder class YourFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage) { Log.d("FCM", "Received message: ${remoteMessage.messageId}") val data = remoteMessage.data val notification = remoteMessage.notification // Extract notification data from Firebase payload val id = data["id"] ?: remoteMessage.messageId ?: System.currentTimeMillis().toString() val title = data["title"] ?: notification?.title ?: "Notification" val body = data["body"] ?: notification?.body ?: "" val imageUrl = data["image"] val imageEnabled = data["imageEnabled"]?.toBoolean() ?: !imageUrl.isNullOrEmpty() val buttonsJson = data["buttons"] val buttonsEnabled = data["buttonsEnabled"]?.toBoolean() ?: false val categoryId = data["categoryId"] val payload = data["payload"] val priority = data["priority"] ?: "defaultPriority" // Use the plugin's helper to show rich notification NotificationBuilder.showRichNotification( context = applicationContext, id = id, title = title, body = body, imageUrl = imageUrl, imageEnabled = imageEnabled, buttonsJson = buttonsJson, buttonsEnabled = buttonsEnabled, categoryId = categoryId, payload = payload, priority = priority ) } override fun onNewToken(token: String) { Log.d("FCM", "New token: $token") // Send token to your server if needed } } -
Register your service in
android/app/src/main/AndroidManifest.xml:<service android:name=".YourFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service> -
Send notifications using the unified payload format in your Firebase data payload:
{ "id": "notification_123", "title": "New Message", "body": "You have a new message", "image": "https://example.com/image.jpg", "imageEnabled": "true", "buttons": "[{\"id\":\"reply\",\"title\":\"Reply\"},{\"id\":\"delete\",\"title\":\"Delete\",\"isDestructive\":true}]", "buttonsEnabled": "true", "payload": "{\"type\":\"message\",\"userId\":\"123\"}", "priority": "high" }
That's it! The NotificationBuilder helper class handles:
- ✅ Creating notification channels
- ✅ Downloading and attaching images
- ✅ Creating action buttons
- ✅ Setting up tap and action receivers
- ✅ All the rich notification features
- ✅ Works in background - Firebase service runs in background when app is closed
Important Notes:
- ✅ Background Support: Yes!
NotificationBuilderworks when notifications are received in the background. The Firebase Messaging Service runs automatically when a push notification arrives, even if the app is closed. - ✅ Foreground Support: Also works when app is in foreground.
- ✅ Terminated App: Works when app is completely closed (killed).
Your Firebase service just needs to extract the data and call NotificationBuilder.showRichNotification().
iOS (APNs with Notification Service Extension)
For iOS rich push notifications with images, you need to set up a Notification Service Extension (NSE). The plugin provides a NotificationBuilder helper class for iOS as well.
Important Notes:
- ✅ Background Support: Yes! Works when notifications are received in background
- ✅ Foreground Support: Works when app is in foreground
- ✅ Terminated App: Works when app is completely closed
- ⚠️ NSE Required: For images in remote push, you MUST use a Notification Service Extension (NSE). This is an iOS requirement.
Option 1: Using Notification Service Extension (Recommended for Images)
This is required if you want images in remote push notifications.
Step 1: Create Notification Service Extension Target
- Open your project in Xcode
- Go to File → New → Target
- Select Notification Service Extension
- Name it (e.g., "NotificationServiceExtension")
- Click Finish and Activate the scheme
Step 2: Update Extension Code
Replace the content of NotificationServiceExtension/NotificationService.swift:
import UserNotifications
import swift_notifications
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
guard let bestAttemptContent = bestAttemptContent else {
contentHandler(request.content)
return
}
// Extract data from notification payload
let userInfo = bestAttemptContent.userInfo
let imageUrl = userInfo["image"] as? String
let imageEnabled = (userInfo["imageEnabled"] as? String) == "true" || imageUrl != nil
let buttonsJson = userInfo["buttons"] as? String
let buttonsEnabled = (userInfo["buttonsEnabled"] as? String) == "true"
let categoryId = userInfo["categoryId"] as? String
// Setup category with buttons if needed
if buttonsEnabled {
NotificationBuilder.setupCategory(
categoryId: categoryId,
buttonsJson: buttonsJson,
buttonsEnabled: buttonsEnabled
)
bestAttemptContent.categoryIdentifier = categoryId ?? "swift_notifications_default"
}
// Download and attach image if enabled
if imageEnabled, let imageUrl = imageUrl, let url = URL(string: imageUrl) {
NotificationBuilder.downloadAndAttachImage(url: url, content: bestAttemptContent) { success in
if !success {
print("Warning: Failed to load image")
}
contentHandler(bestAttemptContent)
}
} else {
contentHandler(bestAttemptContent)
}
}
override func serviceExtensionTimeWillExpire() {
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
Step 3: Configure App Group (Optional, for sharing data)
If you need to share data between app and extension:
- Enable App Groups in both app and extension targets
- Use the same group identifier (e.g.,
group.com.yourapp.notifications)
Step 4: Send Remote Push with Image
When sending remote push from your server, include the image URL in the payload:
{
"aps": {
"alert": {
"title": "New Message",
"body": "You have a new message"
},
"mutable-content": 1
},
"image": "https://example.com/image.jpg",
"id": "notification_123",
"payload": {
"type": "message",
"userId": "123"
}
}
Important: The mutable-content: 1 key is required for NSE to process the notification.
Option 2: Using AppDelegate (For Basic Notifications Without Images)
If you don't need images in remote push, you can handle notifications in your AppDelegate:
import UserNotifications
import swift_notifications
extension AppDelegate: UNUserNotificationCenterDelegate {
// Handle notification when app is in foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
// Setup category with buttons if needed
let userInfo = notification.request.content.userInfo
let buttonsJson = userInfo["buttons"] as? String
let buttonsEnabled = (userInfo["buttonsEnabled"] as? String) == "true"
let categoryId = userInfo["categoryId"] as? String
if buttonsEnabled {
NotificationBuilder.setupCategory(
categoryId: categoryId,
buttonsJson: buttonsJson,
buttonsEnabled: buttonsEnabled
)
}
completionHandler([.banner, .sound, .badge])
}
// Handle notification tap
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
// Handle tap/action - events are automatically handled by the plugin
completionHandler()
}
}
Note: AppDelegate approach works for basic notifications, but for images in remote push, you MUST use NSE (Option 1).
macOS (APNs)
macOS remote push works similarly to iOS but doesn't require NSE. You can handle remote push in your AppDelegate.swift:
import UserNotifications
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
// Handle notification when app is in foreground
completionHandler([.banner, .sound, .badge])
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
// Handle notification tap
let userInfo = response.notification.request.content.userInfo
// Process userInfo and route to appropriate screen
completionHandler()
}
}
Note: For macOS, images in remote push can be attached directly in the delegate methods without requiring an extension.
🧊 Cold-Start Event Handling
The plugin automatically handles notification taps and button clicks even when the app is terminated (cold start):
- Events are stored in persistent storage when app is not running
- Events are delivered when the app starts and Flutter is ready
- Works seamlessly - no additional code required
Just listen to the onNotificationResponse stream as usual:
notifications.onNotificationResponse.listen((response) {
// This will receive events even from cold start
print('Notification: ${response.notificationId}');
print('Action: ${response.actionId}');
});
🎯 Screen Routing with screen_launch_by_notfication
For advanced screen routing and navigation when notifications are tapped (especially from cold start), integrate with the screen_launch_by_notfication plugin.
Why Use screen_launch_by_notfication?
- Automatic routing: Skip splash screens and route directly to notification-specific screens
- Deep link support: Handle custom URL schemes and universal links
- Cold start handling: Works when app is killed, in background, or foreground
- Zero native setup: All native code handled automatically
Integration Example
import 'package:swift_notifications/swift_notifications.dart';
import 'package:screen_launch_by_notfication/screen_launch_by_notfication.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final notifications = SwiftNotifications();
await notifications.initialize();
await notifications.requestPermission();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SwiftFlutterMaterial(
materialApp: MaterialApp(
title: 'My App',
routes: {
'/home': (context) => HomeScreen(),
'/message': (context) => MessageScreen(),
'/order': (context) => OrderScreen(),
},
),
onNotificationLaunch: ({required isFromNotification, required payload}) {
if (isFromNotification && payload.isNotEmpty) {
// Route based on notification type
if (payload['type'] == 'message') {
return SwiftRouting(
route: '/message',
payload: {
'messageId': payload['messageId'],
'senderId': payload['senderId'],
},
);
} else if (payload['type'] == 'order') {
return SwiftRouting(
route: '/order',
payload: {
'orderId': payload['orderId'],
},
);
}
}
return null; // Use default route
},
);
}
}
Handling Notification Button Actions
You can combine both plugins to handle button actions and route accordingly:
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final notifications = SwiftNotifications();
@override
void initState() {
super.initState();
// Listen to notification responses (button clicks, taps)
notifications.onNotificationResponse.listen((response) {
// Handle button actions
if (response.actionId == 'view_order') {
Navigator.pushNamed(context, '/order', arguments: {
'orderId': response.payload?['orderId'],
});
} else if (response.actionId == 'reply') {
Navigator.pushNamed(context, '/message', arguments: {
'messageId': response.payload?['messageId'],
});
}
});
}
@override
Widget build(BuildContext context) {
return SwiftFlutterMaterial(
materialApp: MaterialApp(
// ... your app config
),
onNotificationLaunch: ({required isFromNotification, required payload}) {
// Handle cold start routing
if (isFromNotification) {
return SwiftRouting(
route: '/notification',
payload: payload,
);
}
return null;
},
);
}
}
This combination gives you:
- ✅ Cold start routing via
screen_launch_by_notfication - ✅ Button action handling via
swift_notifications - ✅ Seamless navigation in all app states
🛡️ Error Handling
The plugin gracefully handles errors:
- Image download fails: Falls back to text notification
- Invalid actions: Notification shown without buttons
- Permission denied: Methods return appropriate error status
- Network offline: Image notifications fall back to text
- Cold start events: Automatically stored and delivered when app starts
🐛 Debugging
Enable verbose logging to debug notification issues:
// Enable verbose logging
SwiftNotifications.enableVerboseLogging();
// Or during initialization
await notifications.initialize(verbose: true);
This will print detailed logs about:
- Notification creation
- Image downloads
- Event handling
- Cold-start event storage and retrieval
- Permission status
📝 Examples
See the example/ directory for a complete working example demonstrating:
- Simple notifications
- Image notifications
- Button notifications
- Rich notifications (image + buttons)
- Handling notification responses
- Permission management
- Cold-start event handling
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
📄 License
See the LICENSE file for details.
🙏 Acknowledgments
Built with ❤️ for the Flutter community.
One API. Three Platforms. Zero Native Setup.