The Complete Guide to Local Notifications in Flutter

Notifications are a powerful tool for driving user engagement and retention in mobile apps. When used effectively, they can boost app open rates by up to 40% and increase 90-day retention by 3-10x. (Source)

As a Flutter developer, you have two main options for adding notifications to your app:

  1. Push Notifications – Sent from an external server to a user‘s device, even when the app isn‘t running. Requires additional setup and infrastructure.

  2. Local Notifications – Scheduled and displayed directly from within the app, without needing a network connection or backend service. Ideal for reminders, alerts, and time-sensitive content.

In this guide, we‘ll take a deep dive into local notifications in Flutter. You‘ll learn how to use the flutter_local_notifications package to easily add notifications to your app, customize their appearance and behavior, and handle user interactions. Let‘s get started!

How Local Notifications Work

Under the hood, local notifications are powered by platform-specific APIs and services:

  • On Android, the NotificationManager and AlarmManager classes handle scheduling and displaying notifications. Notifications can be customized with icons, sounds, vibration patterns, and custom layouts.

  • On iOS, the UserNotifications framework is responsible for notifications. Apps must request permission from the user before sending any notifications. Customization options include titles, subtitles, badges, and category identifiers.

The flutter_local_notifications package abstracts away these platform differences and provides a unified Dart API for creating, scheduling, and managing notifications across both Android and iOS.

Adding the Dependency

To use the package in your Flutter project, add this line to your pubspec.yaml file:

dependencies:
  flutter_local_notifications: ^6.0.0

Then run flutter pub get to pull in the latest version. At the time of writing, 6.0.0 is the most recent stable release.

Initializing the Plugin

Before you can display notifications, you need to initialize the plugin and request the necessary permissions. This code typically goes in your main() function:

import ‘package:flutter_local_notifications/flutter_local_notifications.dart‘;

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Android notification channel setup
  const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings(‘app_icon‘);

  // iOS notification setup
  const IOSInitializationSettings initializationSettingsIOS = IOSInitializationSettings(
    requestSoundPermission: false,
    requestBadgePermission: false,
    requestAlertPermission: false,
  );

  // Combined initialization settings  
  const InitializationSettings initializationSettings = InitializationSettings(
    android: initializationSettingsAndroid,
    iOS: initializationSettingsIOS,
  );

  // Initialize the plugin
  await flutterLocalNotificationsPlugin.initialize(initializationSettings);

  runApp(MyApp());
}

On Android, you need to define a notification channel with a unique ID, name, and description. The app_icon parameter specifies the default icon to show in the status bar.

On iOS, you can choose which permissions to request from the user by setting the requestSoundPermission, requestBadgePermission, and requestAlertPermission flags. It‘s recommended to wait until the user takes a relevant action, like tapping a "Enable Notifications" button, before requesting permissions.

Scheduling Notifications

To schedule a notification, you create an instance of the AndroidNotificationDetails or IOSNotificationDetails class to configure the notification‘s appearance and behavior. Then you pass those details to the zonedSchedule() method, along with a unique ID, title, body, and timestamp:

// Android notification details
const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
  ‘channel ID‘,
  ‘channel name‘, 
  ‘channel description‘,
  importance: Importance.max,
  priority: Priority.high,
  color: Colors.blue,
  enableLights: true,
);

// iOS notification details    
const IOSNotificationDetails iosDetails = IOSNotificationDetails(
  presentAlert: true,
  presentBadge: true,
  presentSound: true,
  badgeNumber: 1,
  attachments: null,
  subtitle: ‘This is a longer subtitle‘,
);

// Combined notification details
const NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidDetails, iOS: iosDetails);

// Schedule for 5 seconds from now
final scheduledTime = tz.TZDateTime.now(tz.local).add(Duration(seconds: 5));

// Schedule the notification    
await flutterLocalNotificationsPlugin.zonedSchedule(
  0, // Unique ID
  ‘Scheduled Notification‘, 
  ‘This notification will appear in 5 seconds.‘,
  scheduledTime,
  platformChannelSpecifics,
  uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
  androidAllowWhileIdle: true,
  payload: ‘NOTIFICATION_PAYLOAD‘,
);

The zonedSchedule() method lets you specify an exact timestamp for the notification delivery, taking into account the user‘s local time zone. This ensures notifications appear at the intended time regardless of the user‘s location.

The payload parameter is a string value that you can use to pass custom data to the notification. When the user taps the notification to open your app, you can access this payload to determine what action to take.

Handling Notification Interactions

When a user taps a notification to open your app, you typically want to navigate to a specific screen or perform some action based on the notification‘s contents. To enable this, you need to provide a callback to the onSelectNotification parameter when initializing the plugin:

void main() async {
  // ...

  await flutterLocalNotificationsPlugin.initialize(
    initializationSettings,
    onSelectNotification: onSelectNotification,
  );

  // ...
}

Future<void> onSelectNotification(String payload) async {
  if (payload != null) {
    debugPrint(‘Notification payload: $payload‘);
  }
  // Handle the notification tap action
  // For example: Navigate to a specific screen
}

The onSelectNotification callback receives the payload string that you specified when scheduling the notification. You can parse this payload and use it to determine what action to take in your app.

For example, you might include a screen name or ID in the payload, and use that to navigate to the corresponding screen in your app when the user taps the notification.

Scheduling Multiple Notifications

To schedule multiple notifications, simply call the zonedSchedule() method repeatedly with different IDs, titles, bodies, and timestamps:

for (int i = 0; i < 3; i++) {
  final scheduledTime = tz.TZDateTime.now(tz.local).add(Duration(seconds: i * 5));

  await flutterLocalNotificationsPlugin.zonedSchedule(
    i,
    ‘Notification $i‘,
    ‘This is notification number $i‘,
    scheduledTime,
    platformChannelSpecifics,
    uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
    androidAllowWhileIdle: true,
  );
}

This code schedules 3 notifications, each appearing 5 seconds apart. Note that each notification must have a unique integer ID to avoid conflicts.

Cancelling Notifications

You can cancel a single notification by calling the cancel() method with the notification‘s ID:

await flutterLocalNotificationsPlugin.cancel(0);

To cancel all pending notifications at once, use the cancelAll() method:

await flutterLocalNotificationsPlugin.cancelAll();

Cancelling notifications is useful when the event or condition that triggered the notification is no longer relevant. For example, if you have a notification reminding the user to complete a task, you would cancel that notification when the user completes the task in your app.

Notification Channels and Categories

On Android 8.0 (API level 26) and above, you must assign each notification to a channel. Channels let you group notifications by type and give users fine-grained control over notification behavior.

You can create a channel by passing a unique channel ID, name, and description to the AndroidNotificationDetails constructor:

const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
  ‘channel ID‘,
  ‘channel name‘, 
  ‘channel description‘,
  importance: Importance.max,
  priority: Priority.high,
);

On iOS 10 and above, you can categorize notifications and customize their action buttons using the IOSNotificationDetails class:

const IOSNotificationDetails iosDetails = IOSNotificationDetails(
  categoryIdentifier: ‘CATEGORY_ID‘,
);

You must first register the category ID and its corresponding actions using the requestPermissions() method:

await flutterLocalNotificationsPlugin
    .resolvePlatformSpecificImplementation<
        IOSFlutterLocalNotificationsPlugin>()
    ?.requestPermissions(
      alert: true,
      badge: true,
      sound: true,
      critical: true,
      categories: [
        IOSNotificationCategory(
          identifier: ‘CATEGORY_ID‘,
          actions: [
            IOSNotificationAction(
              identifier: ‘ACTION_ID‘,
              title: ‘Action Button‘,
              options: IOSNotificationActionOptions.destructive,
            ),
          ],
          options: [
            IOSNotificationCategoryOptions.hiddenPreviewShowTitle,
            IOSNotificationCategoryOptions.customDismissAction,
          ],
        ),
      ],
);

This code defines a category with a single action button. When the user taps the button, your app can respond accordingly in the onSelectNotification callback.

Notification Sounds and Icons

To customize the sound that plays when a notification arrives, you can specify a sound file in the AndroidNotificationDetails and IOSNotificationDetails constructors:

const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
  ‘channel ID‘,
  ‘channel name‘, 
  ‘channel description‘,
  sound: RawResourceAndroidNotificationSound(‘sound_file‘),
);

const IOSNotificationDetails iosDetails = IOSNotificationDetails(
  sound: ‘sound_file.aiff‘,
);

On Android, the sound file must be placed in the res/raw directory. On iOS, it must be in the app bundle root.

To use a custom notification icon on Android, specify the icon file name in the AndroidInitializationSettings constructor:

const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings(‘app_icon‘);

The icon file must be placed in the drawable directory for each screen density (e.g. drawable-hdpi, drawable-xhdpi, etc.).

On iOS, you can set the app badge number in the IOSNotificationDetails constructor:

const IOSNotificationDetails iosDetails = IOSNotificationDetails(
  badgeNumber: 1,
);

Testing and Debugging Notifications

When working with local notifications, it‘s important to test on physical devices to ensure they behave as expected. The iOS and Android simulators may not accurately reflect the notification behavior on real devices.

Some common issues to watch out for:

  • Permissions not granted – Make sure your app requests and receives the necessary permissions before scheduling notifications. Test on devices with different OS versions and permission settings.

  • Invalid icon or sound files – Double-check that your icon and sound files are in the correct format and location for each platform. Incorrect file names or unsupported formats can cause notifications to fail silently.

  • Timezone issues – When scheduling notifications, make sure to use the tz package to specify the user‘s local timezone. Scheduling notifications with DateTime.now() can cause issues for users in different timezones.

  • Duplicate or expired notifications – Each notification must have a unique integer ID. Scheduling multiple notifications with the same ID will overwrite the previous one. Similarly, if you schedule a notification with a timestamp in the past, it will fire immediately or not at all.

If notifications aren‘t appearing as expected, you can use the following debugging techniques:

  • Check the console output in Android Studio or Xcode for any error messages or stack traces related to the flutter_local_notifications plugin.
  • Use the print() or debugPrint() statements to log information about your notifications before scheduling them.
  • Manually trigger notifications with specific IDs, titles, bodies, and payloads to isolate any issues.
  • Check the device‘s notification settings to ensure your app hasn‘t been muted or blocked.
  • Use the pendingNotificationRequests() method to retrieve a list of all scheduled notifications and verify their settings.

By testing thoroughly and monitoring the system log output, you can identify and fix any notification-related issues in your Flutter app.

Conclusion

Local notifications are a powerful way to keep users engaged with your Flutter app, even when they‘re not actively using it. With the flutter_local_notifications plugin, you can easily schedule, customize, and manage notifications on both Android and iOS.

Some key points to remember:

  • Initialize the plugin and request the necessary permissions before scheduling any notifications.
  • Use the zonedSchedule() method to schedule notifications at a specific date and time, taking into account the user‘s local timezone.
  • Handle notification taps by providing an onSelectNotification callback and using the payload to determine the appropriate action.
  • Cancel outdated notifications to avoid cluttering the user‘s notification drawer.
  • Test on physical devices and monitor the system logs to identify any issues.

By following the tips and examples in this guide, you‘ll be able to add reliable, engaging notifications to your Flutter app in no time. Happy coding!

Similar Posts