Save Points Notification Builder 🚀

pub package License: MIT

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, and MiuiWarningBanner.
  • 🔋 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!

  1. Create the directory: android/app/src/main/res/drawable/ (if it doesn't exist)
  2. Create a file named ic_notification.xml in that directory
  3. 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:

  1. Android:
    • Place your sound file (e.g., mysound.mp3) in android/app/src/main/res/raw/.
    • Update NotificationScheduler.dart to include sound: RawResourceAndroidNotificationSound('mysound') in AndroidNotificationDetails.
  2. iOS:
    • Add the sound file to your Xcode project.
    • Update NotificationScheduler.dart to include presentSound: true and the sound filename in DarwinNotificationDetails.

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.