Scheduling Notifications to Trigger at Specific Times in Flutter

In Flutter, scheduling local notifications to trigger at specific times is a powerful way to engage users and provide timely updates, reminders, or alerts, even when the app is not actively running in the foreground. Flutter provides mechanisms through plugins that interact with the operating system to achieve this functionality. The combination of the flutter_local_notifications plugin and the timezone package enables you to schedule and display notifications at precise moments.

Understanding Local Notifications

Local notifications are messages that an app displays on a user’s device. These notifications can appear in the system’s notification area, on the lock screen, and can play a sound to get the user’s attention. Unlike push notifications, local notifications do not require a server to send them; they are generated and managed entirely by the app on the user’s device.

Why Schedule Notifications?

  • Reminders: Remind users about upcoming events or tasks.
  • Timely Updates: Provide updates related to user activity.
  • Engagement: Re-engage users with your app at opportune moments.
  • Personalization: Customize notifications based on user preferences and behavior.

Implementation Steps

Step 1: Add Dependencies

First, you need to add the necessary dependencies to your pubspec.yaml file. These include flutter_local_notifications for displaying local notifications and timezone for handling time zones correctly.


dependencies:
  flutter:
    sdk: flutter
  flutter_local_notifications: ^16.3.2
  timezone: ^0.9.2
  rxdart: ^0.27.7 # Required transitive dependency,
  flutter_native_timezone: ^2.0.0

Then, run flutter pub get in the terminal to install these packages.

Step 2: Initialize the Flutter Local Notifications Plugin

You must initialize the plugin to handle local notifications. Create a method to configure the plugin settings for both Android and iOS.


import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:flutter_native_timezone/flutter_native_timezone.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future configureLocalNotifications() async {
  // Initialize timezone data
  tz.initializeTimeZones();

  // Get the local timezone
  final String timeZoneName = await FlutterNativeTimezone.getLocalTimezone();
  tz.setLocalLocation(tz.getLocation(timeZoneName));

  // Android initialization settings
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('app_icon'); // Replace 'app_icon' with the name of your notification icon file

  // iOS initialization settings
  const DarwinInitializationSettings initializationSettingsIOS =
      DarwinInitializationSettings(
    requestAlertPermission: true,
    requestBadgePermission: true,
    requestSoundPermission: true,
  );

  // General initialization settings
  final InitializationSettings initializationSettings = InitializationSettings(
    android: initializationSettingsAndroid,
    iOS: initializationSettingsIOS,
  );

  await flutterLocalNotificationsPlugin.initialize(
    initializationSettings,
    onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) {
          // Handle when the user taps on a notification
          print('Notification payload: ${notificationResponse.payload}');
        },
  );
}

Here’s what this code does:

  • Timezone Configuration: Sets up the timezone information for scheduling notifications accurately. It retrieves the device’s local timezone and configures the timezone package.
  • Initialization Settings: Creates initialization settings for Android and iOS. For Android, it specifies the app icon to use for notifications. For iOS, it requests necessary permissions for alerts, badges, and sounds.
  • Plugin Initialization: Initializes the flutterLocalNotificationsPlugin with the created settings and sets a callback for when a user taps on a notification.

Call this method in your app’s main() function before running the app:


void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await configureLocalNotifications();
  runApp(MyApp());
}

Step 3: Request Permissions

For iOS devices, you need to request permissions from the user to send notifications. You can use the following function:


Future requestPermissions() async {
  if (Platform.isIOS) {
    await flutterLocalNotificationsPlugin
        .resolvePlatformSpecificImplementation<
            IOSFlutterLocalNotificationsPlugin>()
        ?.requestPermissions(
          alert: true,
          badge: true,
          sound: true,
        );
  }
}

Call this method at an appropriate place, such as when the user opens the settings page or at the start of your app, after configuring the notifications.

Step 4: Schedule the Notification

Now, you can schedule a local notification to trigger at a specific time using the zonedSchedule method of the flutterLocalNotificationsPlugin.


Future scheduleNotification({
    required int id,
    required String title,
    required String body,
    required DateTime scheduledDateTime,
  }) async {
    await flutterLocalNotificationsPlugin.zonedSchedule(
      id,
      title,
      body,
      tz.TZDateTime.from(scheduledDateTime, tz.local),
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'channel_id',
          'channel_name',
          channelDescription: 'channel_description',
          importance: Importance.max,
          priority: Priority.high,
          ticker: 'ticker',
        ),
        iOS: DarwinNotificationDetails(),
      ),
      androidAllowWhileIdle: true,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
    );
  }

Here’s a breakdown:

  • Parameters:
    • id: A unique ID for the notification.
    • title: The title of the notification.
    • body: The body text of the notification.
    • scheduledDateTime: The DateTime object representing the time when the notification should be displayed.
  • TZDateTime: The tz.TZDateTime.from method converts the DateTime object to a timezone-aware object. This is essential for scheduling notifications correctly.
  • Notification Details: Configures the look and behavior of the notification, with separate settings for Android and iOS.
  • Android Settings: Includes settings such as the channel ID, importance, priority, and ticker.
  • iOS Settings: Includes settings specific to iOS.
  • androidAllowWhileIdle: Allows the notification to be delivered even when the device is in doze mode.
  • uiLocalNotificationDateInterpretation: Specifies how the date should be interpreted.

Example Usage:


import 'package:flutter/material.dart';

class NotificationScheduler extends StatefulWidget {
  @override
  _NotificationSchedulerState createState() => _NotificationSchedulerState();
}

class _NotificationSchedulerState extends State {
  DateTime? selectedDateTime;

  Future _selectDateTime(BuildContext context) async {
    final DateTime? picked = await showDatePicker(
      context: context,
      initialDate: DateTime.now(),
      firstDate: DateTime.now(),
      lastDate: DateTime(2101),
    );

    if (picked != null) {
      final TimeOfDay? timePicked = await showTimePicker(
        context: context,
        initialTime: TimeOfDay.now(),
      );

      if (timePicked != null) {
        setState(() {
          selectedDateTime = DateTime(
            picked.year,
            picked.month,
            picked.day,
            timePicked.hour,
            timePicked.minute,
          );
        });
        print('Selected date and time: $selectedDateTime');
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Schedule Notification'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => _selectDateTime(context),
              child: Text('Pick Date and Time'),
            ),
            SizedBox(height: 20),
            Text(
              selectedDateTime != null
                  ? 'Selected Date and Time: ${selectedDateTime.toString()}'
                  : 'No date and time selected',
            ),
            ElevatedButton(
              onPressed: selectedDateTime == null ? null : () {
                scheduleNotification(
                  id: 0,
                  title: 'Scheduled Notification',
                  body: 'This notification was scheduled!',
                  scheduledDateTime: selectedDateTime!,
                );
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Notification Scheduled!')),
                );
              },
              child: Text('Schedule Notification!'),
            ),
          ],
        ),
      ),
    );
  }
}

Step 5: Handling Notification Taps

When the user taps on a notification, you might want to navigate them to a specific part of your app. You can achieve this using the onDidReceiveNotificationResponse callback when initializing the plugin.


// General initialization settings
  final InitializationSettings initializationSettings = InitializationSettings(
    android: initializationSettingsAndroid,
    iOS: initializationSettingsIOS,
  );

  await flutterLocalNotificationsPlugin.initialize(
    initializationSettings,
    onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) {
          // Handle when the user taps on a notification
          print('Notification payload: ${notificationResponse.payload}');
          // Add navigation logic here based on the payload.  For example, a Navigator.push
        },
  );

Best Practices

  • Handle Time Zones Correctly: Always use the timezone package to manage timezones and schedule notifications accurately.
  • Provide Clear Instructions: Clearly instruct users on how to enable notifications in the app settings if they are disabled.
  • Respect User Preferences: Allow users to customize the notification settings, such as frequency, type, and sound.
  • Test Thoroughly: Test scheduled notifications on different devices and OS versions to ensure they are delivered reliably.
  • Handle Edge Cases: Handle cases where the user changes the device’s time or timezone.

Conclusion

Scheduling notifications to trigger at specific times in Flutter involves configuring the flutter_local_notifications plugin, handling time zones with the timezone package, requesting necessary permissions, and scheduling notifications using the zonedSchedule method. By following these steps, you can create a reliable and engaging notification system in your Flutter applications, ensuring that users receive timely and relevant updates even when your app is in the background.