swift_notifications 0.0.1
swift_notifications: ^0.0.1 copied to clipboard
A unified Flutter plugin for rich push and local notifications on Android, iOS, and macOS with no native code required.
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.