image

20 Dec 2025

9K

35K

Flutter & Local Notifications: Building a Reminder App

Flutter, Google's UI toolkit for building natively compiled applications from a single codebase, offers developers incredible flexibility and speed. When building applications like reminder tools, task managers, or event schedulers, the ability to deliver timely notifications to users, even when the app is not actively running, is crucial. This is where local notifications come into play. This article will guide you through integrating local notifications into a Flutter application to create a simple yet effective reminder app.

1. Setting Up Your Flutter Project

First, create a new Flutter project and navigate into its directory:


flutter create reminder_app
cd reminder_app

Next, you need to add the flutter_local_notifications and timezone packages to your pubspec.yaml file. The timezone package is essential for accurate scheduling, especially when dealing with different time zones.


dependencies:
  flutter:
    sdk: flutter
  flutter_local_notifications: ^17.1.2
  timezone: ^0.9.0
  intl: ^0.19.0 # For date formatting in UI example

After modifying the pubspec.yaml, run flutter pub get to fetch the new dependencies.


flutter pub get

2. Platform-Specific Configurations

Local notifications require minimal platform-specific setup, but it's important to ensure everything is configured correctly.

Android

  • Min SDK Version: Ensure your android/app/build.gradle file has a minSdkVersion of at least 21 for reliable notification scheduling.

    
            android {
                defaultConfig {
                    minSdkVersion 21 // Or higher
                    // ...
                }
            }
            
  • App Icon: The @mipmap/ic_launcher is typically used as the small icon for notifications. Make sure your app has a proper launcher icon set up.

iOS

  • Info.plist: Add the following key-value pair to your ios/Runner/Info.plist file to provide a privacy description for notification usage. This is required by Apple.

    
            <key>NSUserNotificationsUsageDescription</key>
            <string>This app needs to send you notifications for reminders.</string>
            
  • Push Notifications Capability (Optional but Recommended): While local notifications don't strictly require the "Push Notifications" capability in Xcode, it's good practice to enable it if you foresee expanding to remote push notifications later. Go to your project settings in Xcode > Signing & Capabilities > Add Capability > Push Notifications.

3. Initializing the Notification Plugin

It's best to initialize the notification plugin early in your application's lifecycle, typically in the main.dart file or a dedicated notification service. This involves setting up initialization settings for each platform and registering a callback for when a notification is tapped.


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

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future initNotifications() async {
  // Initialize timezone data
  tz.initializeAll();

  // Android settings
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('@mipmap/ic_launcher');

  // iOS/macOS settings
  final DarwinInitializationSettings initializationSettingsDarwin =
      DarwinInitializationSettings(
    onDidReceiveLocalNotification: (id, title, body, payload) async {
      // Handle notification received in foreground on iOS (optional)
      // For simple reminder app, we might just show a simple alert or do nothing.
      print('iOS foreground notification: id=$id, title=$title, body=$body, payload=$payload');
    },
  );

  // General initialization settings
  final InitializationSettings initializationSettings = InitializationSettings(
    android: initializationSettingsAndroid,
    iOS: initializationSettingsDarwin,
    macOS: initializationSettingsDarwin, // macOS uses Darwin settings
  );

  await flutterLocalNotificationsPlugin.initialize(
    initializationSettings,
    onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async {
      // This callback is called when the user taps on a notification
      // regardless of whether the app is in the foreground, background, or terminated.
      print('Notification tapped: ${notificationResponse.payload}');
      // You can navigate to a specific screen or perform actions based on the payload.
    },
    onDidReceiveBackgroundNotificationResponse: (NotificationResponse notificationResponse) async {
      // This callback is called when the user taps on a notification
      // and the app is in the background or terminated.
      print('Background notification tapped: ${notificationResponse.payload}');
    },
  );

  // Request permissions for iOS (required)
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          IOSFlutterLocalNotificationsPlugin>()
      ?.requestPermissions(
        alert: true,
        badge: true,
        sound: true,
      );
}

4. Scheduling a Reminder Notification

The core functionality of a reminder app is scheduling notifications for a specific time. We'll use the zonedSchedule method from flutter_local_notifications, which relies on the timezone package for accurate time zone handling.


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

Future scheduleReminderNotification(int id, String title, String body, DateTime scheduledTime) async {
  final AndroidNotificationDetails androidPlatformChannelSpecifics =
      AndroidNotificationDetails(
    'reminder_channel', // Channel ID
    'Reminder Channel', // Channel Name
    channelDescription: 'Channel for reminder notifications',
    importance: Importance.max,
    priority: Priority.high,
    ticker: 'ticker', // Used for older Android versions
    // You can customize sound, vibrations, etc. here
  );

  final DarwinNotificationDetails iOSPlatformChannelSpecifics =
      DarwinNotificationDetails(
    // Customize iOS sound, attachments, etc.
  );

  final NotificationDetails platformChannelSpecifics = NotificationDetails(
    android: androidPlatformChannelSpecifics,
    iOS: iOSPlatformChannelSpecifics,
    macOS: iOSPlatformChannelSpecifics,
  );

  // Ensure scheduledTime is in the local timezone for proper scheduling
  await flutterLocalNotificationsPlugin.zonedSchedule(
    id,
    title,
    body,
    tz.TZDateTime.from(scheduledTime, tz.local), // Schedule using local timezone
    platformChannelSpecifics,
    androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, // For accurate scheduling even in Doze mode
    uiLocalNotificationDateInterpretation:
        UILocalNotificationDateInterpretation.absoluteTime,
    payload: 'Reminder Payload: $id', // Custom data associated with the notification
  );
  print('Reminder scheduled for: $scheduledTime (ID: $id)');
}

// Function to cancel a specific notification
Future cancelNotification(int id) async {
  await flutterLocalNotificationsPlugin.cancel(id);
  print('Notification with ID $id cancelled.');
}

// Function to cancel all scheduled notifications
Future cancelAllNotifications() async {
  await flutterLocalNotificationsPlugin.cancelAll();
  print('All notifications cancelled.');
}

5. Building a Simple Reminder App UI

Now, let's create a basic UI that allows users to input a reminder title, body, select a date and time, and then schedule the notification.


import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; // Import for DateFormat

// Assume initNotifications, scheduleReminderNotification are defined in main.dart or a utility file.
// For simplicity, we'll place initNotifications call in MyApp's initState.

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initNotifications(); // Initialize notifications at app startup
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State createState() => _MyAppState();
}

class _MyAppState extends State {
  final TextEditingController _titleController = TextEditingController();
  final TextEditingController _bodyController = TextEditingController();
  DateTime _selectedDateTime = DateTime.now().add(const Duration(seconds: 10)); // Default to 10 seconds from now
  int _notificationId = 0; // Simple incrementing ID for reminders

  Future _selectDateTime(BuildContext context) async {
    final DateTime? pickedDate = await showDatePicker(
      context: context,
      initialDate: _selectedDateTime,
      firstDate: DateTime.now(),
      lastDate: DateTime(2101),
    );
    if (pickedDate != null) {
      final TimeOfDay? pickedTime = await showTimePicker(
        context: context,
        initialTime: TimeOfDay.fromDateTime(_selectedDateTime),
      );
      if (pickedTime != null) {
        setState(() {
          _selectedDateTime = DateTime(
            pickedDate.year,
            pickedDate.month,
            pickedDate.day,
            pickedTime.hour,
            pickedTime.minute,
          );
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Reminder App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Reminder App'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              TextField(
                controller: _titleController,
                decoration: const InputDecoration(
                  labelText: 'Reminder Title',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 16),
              TextField(
                controller: _bodyController,
                decoration: const InputDecoration(
                  labelText: 'Reminder Body',
                  border: OutlineInputBorder(),
                ),
                maxLines: 3,
              ),
              const SizedBox(height: 20),
              Row(
                children: [
                  Expanded(
                    child: Text(
                      'Scheduled for: ${DateFormat('yyyy-MM-dd HH:mm').format(_selectedDateTime)}',
                      style: const TextStyle(fontSize: 16),
                    ),
                  ),
                  ElevatedButton(
                    onPressed: () => _selectDateTime(context),
                    child: const Text('Select Date & Time'),
                  ),
                ],
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  if (_titleController.text.isNotEmpty && _bodyController.text.isNotEmpty) {
                    _notificationId++; // Increment ID for a unique reminder
                    scheduleReminderNotification(
                      _notificationId,
                      _titleController.text,
                      _bodyController.text,
                      _selectedDateTime,
                    );
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('Reminder "${_titleController.text}" scheduled! (ID: $_notificationId)')),
                    );
                    _titleController.clear();
                    _bodyController.clear();
                    setState(() {
                      _selectedDateTime = DateTime.now().add(const Duration(minutes: 5)); // Reset for next reminder
                    });
                  } else {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('Please enter title and body.')),
                    );
                  }
                },
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 12),
                ),
                child: const Text(
                  'Schedule Reminder',
                  style: TextStyle(fontSize: 18),
                ),
              ),
              const SizedBox(height: 10),
              ElevatedButton(
                onPressed: () {
                  cancelAllNotifications();
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('All reminders cancelled!')),
                  );
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.redAccent,
                  padding: const EdgeInsets.symmetric(vertical: 12),
                ),
                child: const Text(
                  'Cancel All Reminders',
                  style: TextStyle(fontSize: 18),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

6. Important Considerations

  • Time Zones: Always use the timezone package and tz.TZDateTime.from(dateTime, tz.local) for scheduling. This ensures your notifications trigger at the correct local time, even if the user changes their device's time zone.

  • Unique IDs: Each notification must have a unique integer ID. In the example, we simply increment _notificationId. For a real app, you might use a database-generated ID or a unique UUID for each reminder.

  • Payloads: The payload string associated with a notification is crucial for handling user taps. When a user taps a notification, the onDidReceiveNotificationResponse callback receives this payload, allowing your app to navigate to a specific screen or perform an action related to that reminder.

  • Cancelling Notifications: It's good practice to provide functionality to cancel individual reminders (using cancel(id)) or all pending reminders (using cancelAll()).

  • Foreground Notifications: The onDidReceiveLocalNotification callback for iOS handles notifications when the app is in the foreground. For Android, foreground notifications typically appear in the system tray by default, but you might want to display a custom in-app banner for a more integrated user experience.

  • Permissions: For iOS, explicitly requesting notification permissions using requestPermissions is mandatory. Android handles this automatically for basic local notifications on newer versions, but a runtime permission request might be needed for certain advanced features or older Android versions.

Conclusion

The flutter_local_notifications package provides a robust and easy-to-use solution for implementing local notifications in your Flutter applications. By combining it with the timezone package, you can build powerful and reliable reminder functionalities, significantly enhancing the user experience. This article covered the basic setup, scheduling, and UI integration for a simple reminder app. You can further extend this by adding features like recurring reminders, custom notification sounds, or more sophisticated reminder management.

Related Articles

Dec 21, 2025

Creating a Step Progress Indicator Widget

Creating a Step Progress Indicator Widget in Flutter In modern applications, guiding users through multi-step processes is crucial for a smooth and intuitive u

Dec 20, 2025

Flutter & Local Notifications: Building a Reminder App

Flutter & Local Notifications: Building a Reminder App Flutter, Google's UI toolkit for building natively compiled applications from a single codebase, offers

Dec 20, 2025

Building a Filter Search Widget in Flutter

Building a Filter Search Widget in Flutter Introduction In modern applications, users frequently interact with large datasets. To enhance user experience and e