A comprehensive guide and demo application for integrating local notifications in Flutter apps. This project serves as a practical reference for developers looking to implement notifications on both Android and iOS platforms.
- Basic instant notifications
- Scheduled notifications with date/time picker
- Repeating/periodic notifications
- Custom notification sounds
- Cancel all notifications
- Handle notification tap responses
- Stream-based notification event handling
- iOS full support with DarwinInitializationSettings
- Runtime permission requests (Android 13+)
- View pending/scheduled notifications list
- Cancel specific notification by ID
- Big picture notifications (file path & drawable)
- Progress bar notifications
- Notification action buttons (Reply, Mark as Read, Dismiss)
- Grouped notifications with inbox style
- Enhanced notification details page
- Notification history with search and filter
- Deep linking from notifications
- Media style notifications (for music/audio apps)
All planned features have been implemented!
This project includes comprehensive widget tests. Run tests with:
flutter testCurrent test coverage includes:
- NotificationButton widget tests
- NotificationPage widget tests
- PendingNotificationsPage widget tests
- HeaderCard widget tests
- Main app integration tests
| Platform | Minimum Version |
|---|---|
| Flutter SDK | 3.6.0+ |
| Dart SDK | 3.6.0+ |
| Android | API 21 (Android 5.0) |
| iOS | 13.0+ |
dependencies:
flutter_local_notifications: ^19.5.0
timezone: ^0.10.0flutter pub getAdd permissions above the <application> tag:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>Add receivers inside <application> tag:
<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>Enable desugaring for scheduled notifications:
android {
compileSdk = 35
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
minSdk = 21
targetSdk = 35
multiDexEnabled true
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
}import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest_all.dart' as tz;
class NotificationHelper {
static final FlutterLocalNotificationsPlugin _notification =
FlutterLocalNotificationsPlugin();
static Future<void> init() async {
// Android settings
const androidSettings = AndroidInitializationSettings("@mipmap/ic_launcher");
// iOS settings (recommended)
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _notification.initialize(
initSettings,
onDidReceiveNotificationResponse: onNotificationTap,
onDidReceiveBackgroundNotificationResponse: onNotificationTap,
);
tz.initializeTimeZones();
}
}await NotificationHelper.showBasicNotification(
id: 1,
title: "Hello!",
body: "This is a basic notification",
payload: "custom_data",
);await NotificationHelper.showScheduleNotification(
id: 2,
title: "Reminder",
body: "Don't forget your task!",
delay: Duration(hours: 1),
);await NotificationHelper.showRepeatingNotification(
id: 3,
title: "Daily Reminder",
body: "Time to check in!",
repeatInterval: RepeatInterval.daily,
);@override
void initState() {
super.initState();
_subscription = NotificationHelper.notificationResponseController.stream
.listen((response) {
// Navigate or handle the tap
Navigator.push(context, MaterialPageRoute(
builder: (context) => NotificationDetailsPage(payload: response.payload),
));
});
}
@override
void dispose() {
_subscription?.cancel(); // Important: prevent memory leaks!
super.dispose();
}Place your sound file in android/app/src/main/res/raw/ (e.g., custom_sound.mp3)
NotificationHelper.showBasicNotification(
id: 4,
title: "Custom Sound",
body: "This notification has a custom sound!",
sound: RawResourceAndroidNotificationSound('custom_sound'), // No extension!
);// From file path
await NotificationHelper.showBigPictureNotification(
id: 5,
title: "New Photo",
body: "Check out this amazing picture!",
bigPicturePath: "/path/to/image.jpg",
summaryText: "Photo from vacation",
);
// From drawable resource
await NotificationHelper.showBigPictureFromDrawable(
id: 6,
title: "App Update",
body: "New features available!",
drawableName: "update_banner",
);// Show progress (useful for downloads, uploads, etc.)
for (int i = 0; i <= 100; i += 10) {
await NotificationHelper.showProgressNotification(
id: 7,
title: "Downloading...",
body: "$i% complete",
progress: i,
maxProgress: 100,
);
await Future.delayed(Duration(milliseconds: 500));
}
// Indeterminate progress (unknown duration)
await NotificationHelper.showProgressNotification(
id: 8,
title: "Processing...",
body: "Please wait",
progress: 0,
maxProgress: 100,
indeterminate: true,
);// Get list of scheduled notifications
final pendingNotifications = await NotificationHelper.getPendingNotifications();
for (final notification in pendingNotifications) {
print('ID: ${notification.id}, Title: ${notification.title}');
}
// Cancel specific notification
await NotificationHelper.cancelNotification(notificationId);// Show notification with Reply, Mark as Read, and Dismiss buttons
await NotificationHelper.showNotificationWithActions(
id: 9,
title: "New Message",
body: "You have a new message from Ahmed",
payload: "message_123",
showReplyAction: true,
showMarkReadAction: true,
);
// Handle action responses in your listener
NotificationHelper.notificationResponseController.stream.listen((response) {
if (response.actionId == NotificationHelper.actionReply) {
final replyText = response.input;
print('User replied: $replyText');
} else if (response.actionId == NotificationHelper.actionMarkRead) {
print('Marked as read');
}
});// Show multiple notifications grouped together
await NotificationHelper.showNotificationGroup(
groupKey: 'messages_group',
summaryTitle: 'New Messages',
notifications: [
{'title': 'Ahmed', 'body': 'Hey, how are you?'},
{'title': 'Sara', 'body': 'Meeting at 3 PM'},
{'title': 'Ali', 'body': 'Check this out!'},
],
);
// Or show individual grouped notifications
await NotificationHelper.showGroupedNotification(
groupKey: 'emails_group',
title: 'New Email',
body: 'You have a new email from support',
id: 100,
isSummary: false,
);// Get notification history
final history = await NotificationStorageHelper.getNotificationHistory();
// Search notifications
final results = await NotificationStorageHelper.searchNotifications('meeting');
// Get unread count
final unreadCount = await NotificationStorageHelper.getUnreadCount();
// Mark as read
await NotificationStorageHelper.markAsRead(notificationId);
// Clear all history
await NotificationStorageHelper.clearHistory();// Create payloads for deep linking
final pagePayload = DeepLinkHelper.createPagePayload('history'); // Opens history page
final messagePayload = DeepLinkHelper.createMessagePayload('123'); // Opens message
final actionPayload = DeepLinkHelper.createActionPayload('reply', 'Hello');
// Show notification with deep link
await NotificationHelper.showBasicNotification(
id: 10,
title: "View History",
body: "Tap to see your notification history",
payload: pagePayload, // Will navigate to history page
);
// Handle deep links in your listener
DeepLinkHelper.handleDeepLink(
context: context,
response: notificationResponse,
);// Simple media notification (for music apps)
await NotificationHelper.showSimpleMediaNotification(
id: 999,
songTitle: "Beautiful Day",
artist: "Artist Name",
album: "Album Name",
isPlaying: true,
);
// Full media notification with album art
await NotificationHelper.showMediaNotification(
id: 999,
title: "Now Playing",
body: "Album Name",
artist: "Artist Name",
albumArt: "/path/to/album/art.jpg",
isPlaying: true,
);lib/
├── main.dart # App entry point
├── helpers/
│ ├── deep_link_helper.dart # Deep linking from notifications
│ ├── notification_helper.dart # Core notification logic
│ ├── notification_storage_helper.dart # History storage
│ ├── permission_helper.dart # Permission management
│ └── show_snack_bar_helper.dart # UI helper
├── models/
│ └── notification_record.dart # Notification history model
├── pages/
│ ├── home_page.dart # Main UI with buttons
│ ├── notification_history_page.dart # View notification history
│ ├── notification_page.dart # Notification details
│ └── pending_notifications_page.dart # View scheduled notifications
├── widgets/
│ ├── notification_button.dart # Reusable button with icon
│ └── header_card.dart # Header card widget
└── theme/
└── theme.dart # App theming
| Channel ID | Purpose |
|---|---|
basic_notification |
Instant notifications |
repeating_notification |
Periodic notifications |
schedule_notification |
Time-scheduled notifications |
big_picture_notification |
Notifications with images |
progress_notification |
Progress bar notifications |
action_notification |
Notifications with action buttons |
grouped_notification |
Grouped/inbox style notifications |
media_notification |
Media playback controls |
Request POST_NOTIFICATIONS permission at runtime:
final plugin = FlutterLocalNotificationsPlugin();
await plugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()?.requestNotificationsPermission();- Ensure desugaring is enabled in build.gradle
- Call
tz.initializeTimeZones()before scheduling - Check that
SCHEDULE_EXACT_ALARMpermission is granted
- File must be in
res/raw/folder - Use lowercase filename with underscores
- Don't include file extension in code
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
Mahmoud Hamdy
- GitHub: @mahmoodhamdi
- Email: hmdy7486@gmail.com
If you found this guide helpful, please give it a star on GitHub!
مَن قالَ: لا إلَهَ إلَّا اللَّهُ، وحْدَهُ لا شَرِيكَ له، له المُلْكُ وله الحَمْدُ، وهو علَى كُلِّ شَيءٍ قَدِيرٌ، في يَومٍ مِئَةَ مَرَّةٍ؛ كانَتْ له عَدْلَ عَشْرِ رِقابٍ، وكُتِبَتْ له مِئَةُ حَسَنَةٍ، ومُحِيَتْ عنْه مِئَةُ سَيِّئَةٍ، وكانَتْ له حِرْزًا مِنَ الشَّيْطانِ يَومَهُ ذلكَ حتَّى يُمْسِيَ، ولَمْ يَأْتِ أحَدٌ بأَفْضَلَ ممَّا جاءَ به، إلَّا أحَدٌ عَمِلَ أكْثَرَ مِن ذلكَ.
— صحيح البخاري