Push Notifications & Background Services

Displaying Local Notifications with flutter_local_notifications

16 min Lesson 5 of 11

Displaying Local Notifications with flutter_local_notifications

Local notifications allow your Flutter app to alert users with messages, reminders, or updates — entirely from Dart code, without requiring a remote server. The flutter_local_notifications plugin is the de facto standard for scheduling and displaying local notifications on both Android and iOS. In this lesson you will set the plugin up from scratch, initialise it correctly for each platform, and fire your first notification.

Adding the Dependency

Open pubspec.yaml and add the plugin under dependencies:

dependencies:
  flutter:
    sdk: flutter
  flutter_local_notifications: ^17.0.0   # use latest stable

Run flutter pub get to download the package. On Android no additional Gradle changes are needed for basic notifications; on iOS you must request permission at runtime (covered below).

Note: The plugin version number evolves quickly. Always check pub.dev for the current stable release and review its changelog before upgrading, as there are occasionally breaking API changes between major versions.

Android Platform Setup

For Android 13 (API 33) and above, the POST_NOTIFICATIONS permission must be declared and requested at runtime. Add it to android/app/src/main/AndroidManifest.xml inside the <manifest> tag:

<!-- Required for Android 13+ -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!-- Required to schedule exact alarms on Android 12+ -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />

You must also set the minimum SDK version to at least 21 in android/app/build.gradle:

android {
    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 34
    }
}

iOS Platform Setup

On iOS, open ios/Runner/AppDelegate.swift and ensure it calls super.application(_:didFinishLaunchingWithOptions:) so the plugin can register its notification handlers. No additional Info.plist keys are needed for local notifications; the plugin requests permission through the system dialog at runtime.

Initialising the Plugin

Initialise FlutterLocalNotificationsPlugin once during app startup — typically in main() before runApp(). You create an initialisation settings object for each platform and combine them into a single InitializationSettings instance.

import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/material.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // --- Android init settings ---
  const AndroidInitializationSettings androidSettings =
      AndroidInitializationSettings('@mipmap/ic_launcher');

  // --- iOS init settings ---
  const DarwinInitializationSettings iosSettings =
      DarwinInitializationSettings(
    requestAlertPermission: true,
    requestBadgePermission: true,
    requestSoundPermission: true,
  );

  const InitializationSettings initSettings = InitializationSettings(
    android: androidSettings,
    iOS: iosSettings,
  );

  await flutterLocalNotificationsPlugin.initialize(
    initSettings,
    onDidReceiveNotificationResponse: (NotificationResponse response) {
      // Called when the user taps the notification
      debugPrint('Notification tapped. Payload: ${response.payload}');
    },
  );

  runApp(const MyApp());
}
Tip: WidgetsFlutterBinding.ensureInitialized() is required before any async work in main(). Without it, the Flutter engine binding is not yet initialised and plugin channels will throw a MissingPluginException.

Requesting Runtime Permission (Android 13+ / iOS)

On iOS and Android 13+, you must ask the user for permission before showing notifications. The plugin exposes a convenience method to do this:

Future<void> requestNotificationPermissions() async {
  // iOS
  final bool? grantedIOS = await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          IOSFlutterLocalNotificationsPlugin>()
      ?.requestPermissions(
        alert: true,
        badge: true,
        sound: true,
      );

  // Android 13+
  final bool? grantedAndroid = await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          AndroidFlutterLocalNotificationsPlugin>()
      ?.requestNotificationsPermission();

  debugPrint('iOS granted: $grantedIOS, Android granted: $grantedAndroid');
}
Warning: Call the permission request after a user-facing action (such as tapping a button labelled "Enable Notifications") rather than immediately on app launch. Prompting without context drastically reduces the opt-in rate and may violate app store guidelines.

Showing a Basic Local Notification

Once the plugin is initialised and permission is granted, displaying a notification requires three objects: an AndroidNotificationDetails, a DarwinNotificationDetails (iOS), and a combined NotificationDetails. Call show() with an integer ID, title, body, and optional payload string.

Future<void> showSimpleNotification({
  required int id,
  required String title,
  required String body,
  String? payload,
}) async {
  const AndroidNotificationDetails androidDetails =
      AndroidNotificationDetails(
    'general_channel',          // channel ID (unique per app)
    'General Notifications',    // channel name (shown in Settings)
    channelDescription: 'General app notifications',
    importance: Importance.max,
    priority: Priority.high,
    ticker: 'ticker',
  );

  const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
    presentAlert: true,
    presentBadge: true,
    presentSound: true,
  );

  const NotificationDetails notificationDetails = NotificationDetails(
    android: androidDetails,
    iOS: iosDetails,
  );

  await flutterLocalNotificationsPlugin.show(
    id,
    title,
    body,
    notificationDetails,
    payload: payload,
  );
}

// Usage example:
// await showSimpleNotification(
//   id: 0,
//   title: 'Hello from Flutter!',
//   body: 'This is your first local notification.',
//   payload: 'home_screen',
// );
Note: On Android 8.0 (API 26) and above, every notification must be assigned to a notification channel. The channel ID you pass ('general_channel' above) must be consistent across calls — the system creates the channel on first use. Changing the channel ID creates a new, separate channel visible in the device's Settings app.

Handling Notification Taps and Payloads

The payload parameter is a free-form string you attach to the notification. When the user taps it, the onDidReceiveNotificationResponse callback (registered during initialize()) fires with a NotificationResponse object whose .payload property holds your string. Use it to navigate to the correct screen:

// Inside your Navigator-aware widget (e.g. a service with a GlobalKey)
void handleNotificationTap(NotificationResponse response) {
  final String? payload = response.payload;
  if (payload == 'home_screen') {
    navigatorKey.currentState?.pushNamed('/home');
  } else if (payload == 'settings') {
    navigatorKey.currentState?.pushNamed('/settings');
  }
}

Summary

In this lesson you learned the complete setup flow for flutter_local_notifications:

  • Add the dependency and configure AndroidManifest.xml / build.gradle for Android
  • Create platform-specific initialisation settings (AndroidInitializationSettings, DarwinInitializationSettings)
  • Call FlutterLocalNotificationsPlugin.initialize() in main() after WidgetsFlutterBinding.ensureInitialized()
  • Request runtime permissions on iOS and Android 13+
  • Define a notification channel (Android only) and call show() with an ID, title, body, and optional payload
  • Handle tap events via the onDidReceiveNotificationResponse callback
Next steps: Once basic notifications work, you can extend them to scheduled notifications (zonedSchedule()), recurring notifications (periodicallyShow()), and rich notifications with images, action buttons, or progress bars — all covered in upcoming lessons.