Save Points Notification Builder 🚀
A powerful and easy-to-use Flutter package for building and managing scheduled notifications. Designed with reliability in mind, specifically addressing common issues like MIUI battery optimizations and providing a modern stream-based API for notification interactions.
✨ Features
- 📅 Flexible Scheduling: Support for one-time, daily, weekly, monthly, and yearly notifications.
- 👆 Interactive: Modern
Stream-based API to handle notification taps globally. - 🔄 Persistence: Automatically saves scheduled notifications using
SharedPreferences. - 🏗️ State Management: Built with Riverpod for robust and reactive state handling.
- 🧩 Modular Architecture: Cleanly separated logic for permissions, scheduling, timezone handling, and background processing.
- 📱 UI Components Included: Comes with ready-to-use
NotificationListScreen,NotificationSchedulerSheet, andMiuiWarningBanner. - 🔋 Battery Optimization Aware: Includes built-in instructions for aggressive battery optimization on devices like Xiaomi (MIUI).
- 🛠️ Developer Friendly: Simple API for scheduling, canceling, and testing notifications.
🚀 Quick Start (4 Simple Steps)
📖 Need detailed setup instructions? Check out the SETUP.md guide for step-by-step instructions and troubleshooting.
1. Platform Setup
Android
Step 1: Add Permissions
Add the following to your android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
</manifest>
Step 2: Add Notification Icon (REQUIRED) ⚠️
This step is mandatory - without it, you'll get a PlatformException(invalid_icon) error!
- Create the directory:
android/app/src/main/res/drawable/(if it doesn't exist) - Create a file named
ic_notification.xmlin that directory - Copy this content into the file:
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
</vector>
Alternative: You can also use a PNG icon (white with transparency, 24x24dp recommended) named ic_notification.png instead.
Quick Setup (Automated): Download and run our setup script:
curl -o setup.sh https://raw.githubusercontent.com/m7hamed-dev/save_points_background_service_builder/main/assets/setup.sh && chmod +x setup.sh && ./setup.sh
💡 Tip: The icon file is also available in the package's assets/android folder for easy copying.
iOS
Add this to your ios/Runner/AppDelegate.swift:
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
2. Provider Initialization
Wrap your app with ProviderScope and override the sharedPreferencesProvider. You can also optionally override notificationConfigProvider to provide initial notifications.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize SharedPreferences
final prefs = await SharedPreferences.getInstance();
runApp(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
// OPTIONAL: Provide initial configuration
notificationConfigProvider.overrideWithValue(
const SavePointsNotificationConfig(
initialNotifications: [
NotificationModel(
id: 'welcome',
title: 'Welcome! 👋',
body: 'Thanks for using our app!',
scheduleTime: DateTime.now(), // This is just a placeholder
type: NotificationScheduleType.oneTime,
),
],
),
),
],
child: const MyApp(),
),
);
}
3. Initialize System (Permissions)
Trigger the internal setup (Timezones & Permission requests) once your app launches. This is usually done in your Home Screen's initState.
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// This requests permissions and sets up local timezones
ref.read(notificationsProvider.notifier).initializeSystem();
});
}
4. Schedule & Handle Taps
Schedule a Notification:
final reminder = NotificationModel(
id: 'unique_id_123',
title: 'Hello! 🚀',
body: 'This is your scheduled reminder.',
scheduleTime: DateTime.now().add(const Duration(hours: 1)),
type: NotificationScheduleType.oneTime,
payload: 'any_custom_data',
);
ref.read(notificationsProvider.notifier).addNotification(reminder);
Listen to Taps Globally:
// Use this in any ConsumerWidget to react when a user clicks a notification
ref.listen(notificationTapProvider, (previous, next) {
next.whenData((response) {
print('Notification Tapped! Payload: ${response.payload}');
// Navigate to a specific screen here
});
});
🎨 Customization
Notification Icon
The package looks for a drawable resource named ic_notification by default (see Platform Setup above for how to add it).
- Change Icon Name: If you wish to use a different name, you can modify the constant in your own code or fork the package and update
NotificationConstants.notificationIcon.
Notification Sound
By default, notifications use the system's default sound. To use a custom sound:
- Android:
- Place your sound file (e.g.,
mysound.mp3) inandroid/app/src/main/res/raw/. - Update
NotificationScheduler.dartto includesound: RawResourceAndroidNotificationSound('mysound')inAndroidNotificationDetails.
- Place your sound file (e.g.,
- iOS:
- Add the sound file to your Xcode project.
- Update
NotificationScheduler.dartto includepresentSound: trueand the sound filename inDarwinNotificationDetails.
Note: For Android, you must create a new Notification Channel or delete the existing app to see sound changes, as channels are immutable once created.
🛠️ Advanced Usage
Modular Architecture
The core notification logic is now split into specialized components for better maintainability:
NotificationService: The main high-level API.NotificationScheduler: Handles the complex date logic for different recurrence types.NotificationTimezoneHelper: Manages timezone initialization and local location detection.NotificationPermissionHandler: Handles platform-specific permission requests (Android 13+, iOS).NotificationChannelManager: Manages Android-specific notification channels.NotificationBackgroundHandler: Entry point for background notification interactions.
Using the Built-in UI
The package includes a full management suite via NotificationListScreen. You can use it with the default state or provide your own external list of notifications.
Basic Usage (Using Global State):
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => NotificationListScreen(
itemBuilder: (context, notification) => ListTile(
title: Text(notification.title),
subtitle: Text(notification.body),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => ref.read(notificationsProvider.notifier).removeNotification(notification.id),
),
),
),
),
);
Custom Usage (External List & Custom FAB):
NotificationListScreen(
title: 'My Custom Reminders',
notifications: myExternalList, // List<NotificationModel>
showFab: true,
onAddPressed: () {
// Custom logic when "+" is clicked
},
itemBuilder: (context, notification) {
return MyCustomNotificationCard(notification: notification);
},
)
Syncing External Lists: If you have an external source of truth, you can sync it with the local repository:
await ref.read(notificationsProvider.notifier).setNotifications(myExternalList);
MIUI & Battery Optimization
Many devices (Xiaomi, Huawei, Samsung) kill background alarms by default. We include a MiuiWarningBanner that helps users configure their device settings (Autostart, No Restrictions) to ensure notifications arrive on time.
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.