Building a Multi-Event Countdown Timer Widget with Reminders and Notifications in Flutter
Countdown timers are a popular UI element, useful for tracking upcoming events, deadlines, or special occasions. A multi-event countdown timer takes this concept further, allowing users to monitor several events simultaneously, enhanced with intelligent reminders and notifications to ensure no important date is missed. This article delves into creating such a robust widget in Flutter, covering event data modeling, real-time countdown logic, and effective local notification scheduling.
1. Project Setup and Dependencies
First, create a new Flutter project and add the necessary dependencies to your pubspec.yaml file.
We'll primarily need flutter_local_notifications for handling notifications.
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
flutter_local_notifications: ^17.1.2 # Use the latest stable version
intl: ^0.19.0 # For date formatting, if needed
After adding, run flutter pub get.
2. Initializing Local Notifications
Before scheduling any notifications, flutter_local_notifications must be initialized.
This typically happens in your main.dart file.
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings initializationSettingsIOS =
DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async {
// Handle notification tap here if needed
debugPrint('Notification tapped: ${notificationResponse.payload}');
},
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Multi-Event Countdown',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MultiEventCountdownScreen(),
);
}
}
3. Defining the Event Model
An Event class will hold the details for each countdown item.
It should include a unique ID, name, target date and time, and potentially a notification preference.
class Event {
final int id;
final String name;
final DateTime targetDateTime;
final Duration? reminderOffset; // How long before the event to remind
Event({
required this.id,
required this.name,
required this.targetDateTime,
this.reminderOffset,
});
}
4. Implementing the Countdown Logic
The core of our widget is the countdown logic. We'll need a StatefulWidget that updates
periodically to reflect the remaining time.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; // For date formatting
class EventCountdownWidget extends StatefulWidget {
final Event event;
const EventCountdownWidget({super.key, required this.event});
@override
State createState() => _EventCountdownWidgetState();
}
class _EventCountdownWidgetState extends State {
late Duration _timeRemaining;
Timer? _timer;
@override
void initState() {
super.initState();
_startTimer();
}
void _startTimer() {
_timeRemaining = widget.event.targetDateTime.difference(DateTime.now());
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
setState(() {
_timeRemaining = widget.event.targetDateTime.difference(DateTime.now());
if (_timeRemaining.isNegative) {
_timeRemaining = Duration.zero;
_timer?.cancel();
}
});
}
});
}
String _formatDuration(Duration duration) {
if (duration == Duration.zero) return 'Event has passed!';
String twoDigits(int n) => n.toString().padLeft(2, '0');
final days = duration.inDays;
final hours = twoDigits(duration.inHours.remainder(24));
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$days d $hours h $minutes m $seconds s';
}
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.event.name,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'Target: ${DateFormat.yMMMd().add_Hms().format(widget.event.targetDateTime)}',
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 16),
Text(
_formatDuration(_timeRemaining),
style: TextStyle(
fontSize: _timeRemaining == Duration.zero ? 18 : 24,
color: _timeRemaining == Duration.zero ? Colors.redAccent : Colors.blue,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
}
5. Scheduling Notifications and Reminders
This is where flutter_local_notifications shines. We can schedule one-time notifications
for the exact event time and also for custom reminder times before the event.
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'event_model.dart'; // Make sure this path is correct
class NotificationService {
final FlutterLocalNotificationsPlugin notificationsPlugin;
NotificationService({required this.notificationsPlugin}) {
tz.initializeTimeZones();
}
Future scheduleEventNotification(Event event) async {
final scheduledTime = tz.TZDateTime.from(
event.targetDateTime,
tz.local,
);
if (scheduledTime.isBefore(tz.TZDateTime.now(tz.local))) {
// Don't schedule notifications for past events
return;
}
// Schedule notification for the event itself
await notificationsPlugin.zonedSchedule(
event.id, // Unique ID for this notification
'Event Reminder: ${event.name}',
'It\'s time for "${event.name}"!',
scheduledTime,
const NotificationDetails(
android: AndroidNotificationDetails(
'event_channel_id',
'Event Notifications',
channelDescription: 'Notifications for upcoming events',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
),
iOS: DarwinNotificationDetails(),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.dayOfMonthAndTime,
payload: 'event_id_${event.id}',
);
// Schedule reminder notification if offset is provided
if (event.reminderOffset != null) {
final reminderTime = scheduledTime.subtract(event.reminderOffset!);
if (reminderTime.isAfter(tz.TZDateTime.now(tz.local))) {
await notificationsPlugin.zonedSchedule(
event.id + 1000, // Use a different ID for the reminder
'Upcoming: ${event.name}',
'${event.name} is starting in ${event.reminderOffset!.inMinutes} minutes!',
reminderTime,
const NotificationDetails(
android: AndroidNotificationDetails(
'reminder_channel_id',
'Event Reminders',
channelDescription: 'Reminders before events',
importance: Importance.defaultImportance,
priority: Priority.defaultPriority,
),
iOS: DarwinNotificationDetails(),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.dayOfMonthAndTime,
payload: 'reminder_event_id_${event.id}',
);
}
}
}
Future cancelEventNotifications(int eventId) async {
await notificationsPlugin.cancel(eventId); // Cancel event notification
await notificationsPlugin.cancel(eventId + 1000); // Cancel reminder notification
}
}
Remember to add timezone to your pubspec.yaml:
dependencies:
...
timezone: ^0.9.0
6. The Multi-Event Countdown Screen
Finally, we'll create a screen to display a list of events and manage their notifications.
import 'package:flutter/material.dart';
import 'event_model.dart';
import 'countdown_widget.dart';
import 'notification_service.dart'; // Adjust path as needed
class MultiEventCountdownScreen extends StatefulWidget {
const MultiEventCountdownScreen({super.key});
@override
State createState() => _MultiEventCountdownScreenState();
}
class _MultiEventCountdownScreenState extends State {
final List _events = [];
late final NotificationService _notificationService;
@override
void initState() {
super.initState();
_notificationService = NotificationService(notificationsPlugin: flutterLocalNotificationsPlugin);
_loadSampleEvents();
}
void _loadSampleEvents() {
// Add some sample events
_addEvent(Event(
id: 1,
name: 'Project Deadline',
targetDateTime: DateTime.now().add(const Duration(days: 3, hours: 10, minutes: 30)),
reminderOffset: const Duration(hours: 1), // Remind 1 hour before
));
_addEvent(Event(
id: 2,
name: 'Birthday Party',
targetDateTime: DateTime.now().add(const Duration(days: 7, hours: 18)),
reminderOffset: const Duration(days: 1), // Remind 1 day before
));
_addEvent(Event(
id: 3,
name: 'Vacation Start',
targetDateTime: DateTime.now().add(const Duration(days: 30)),
reminderOffset: const Duration(days: 7), // Remind 1 week before
));
}
void _addEvent(Event event) {
setState(() {
_events.add(event);
});
_notificationService.scheduleEventNotification(event);
}
void _removeEvent(Event event) {
setState(() {
_events.removeWhere((e) => e.id == event.id);
});
_notificationService.cancelEventNotifications(event.id);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Multi-Event Countdown'),
),
body: _events.isEmpty
? const Center(
child: Text(
'No events scheduled. Add a new event!',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
)
: ListView.builder(
itemCount: _events.length,
itemBuilder: (context, index) {
final event = _events[index];
return Dismissible(
key: ValueKey(event.id),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
_removeEvent(event);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${event.name} dismissed')),
);
},
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
child: EventCountdownWidget(event: event),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Implement a dialog or new screen to add events dynamically
// For now, let's just add a placeholder event
final newId = _events.isEmpty ? 1 : _events.last.id + 1;
final newEvent = Event(
id: newId,
name: 'New Custom Event $newId',
targetDateTime: DateTime.now().add(const Duration(minutes: 5) * newId),
reminderOffset: const Duration(minutes: 1),
);
_addEvent(newEvent);
},
child: const Icon(Icons.add),
),
);
}
}
7. Refinements and Considerations
- State Management: For more complex applications, consider using state management solutions like Provider, Riverpod, or BLoC to manage the list of events and their updates more efficiently.
-
Persistent Storage: Events should ideally be stored persistently using solutions
like
shared_preferences, SQLite (viasqflite), or a cloud database. When the app restarts, scheduled notifications for future events need to be re-registered after fetching them from storage. -
Notification Customization: Explore more advanced features of
flutter_local_notifications, such as custom sounds, vibration patterns, large icon support, and action buttons. - User Experience: Provide clear UI for adding, editing, and deleting events. Consider allowing users to customize reminder times.
-
Background Execution: While
flutter_local_notificationshandles background scheduling, if your app needs to perform complex logic or fetch data in the background to update notifications, you might needworkmanageror platform-specific background tasks. -
Time Zone Handling: The
timezonepackage is crucial for correctly scheduling notifications irrespective of the user's current time zone. Ensure proper initialization and usage.
Conclusion
Building a multi-event countdown timer with reminders and notifications in Flutter involves careful integration of UI updates, event data management, and platform-specific notification services. By following the steps outlined, you can create a highly functional and engaging widget that keeps users informed about their important dates and deadlines. Further enhancements can include rich UI animations, category-based event filtering, and synchronization with calendar services for a truly comprehensive experience.