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.gradlefile has aminSdkVersionof at least21for reliable notification scheduling.android { defaultConfig { minSdkVersion 21 // Or higher // ... } } -
App Icon: The
@mipmap/ic_launcheris 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.plistfile 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
timezonepackage andtz.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
payloadstring associated with a notification is crucial for handling user taps. When a user taps a notification, theonDidReceiveNotificationResponsecallback 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 (usingcancelAll()). -
Foreground Notifications: The
onDidReceiveLocalNotificationcallback 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
requestPermissionsis 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.