Building a Multi-Event Countdown Timer Widget with Notification Reminder in Flutter
Countdown timers are essential in many applications, from tracking project deadlines and upcoming meetings to reminding users of special events or product launches. While a single countdown timer is straightforward to implement, managing multiple distinct countdowns simultaneously, each with its own target date and time, adds a layer of complexity. Furthermore, integrating local notification reminders for these events significantly enhances user experience, ensuring critical moments are not missed.
This article will guide you through building a robust Flutter widget that displays multiple event countdown timers and incorporates local notification reminders when an event's target time arrives. We'll cover the data model, timer logic, UI implementation, and local notification integration.
Core Concepts
Before diving into the code, let's understand the core components:
- Event Data Model: A structured way to represent each countdown event, including its title, target date/time, and a unique identifier.
- Timer Management: Using Flutter's
Timer.periodicto efficiently update the remaining time for all events every second. - Local Notifications: Leveraging the
flutter_local_notificationspackage to schedule and display non-disruptive alerts when an event's countdown finishes. - Flutter UI: A dynamic list (
ListView.builder) to display each event's countdown in a user-friendly format.
Step-by-Step Implementation
1. Project Setup and Dependencies
First, create a new Flutter project and add the necessary dependencies to your pubspec.yaml file:
flutter create multi_countdown_app
cd multi_countdown_app
Then, update pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_local_notifications: ^17.1.2 # Use the latest version
intl: ^0.19.0 # Use the latest version for date/time formatting
Run flutter pub get to fetch the new dependencies.
2. Event Data Model (`countdown_event.dart`)
Let's define a simple class to represent our countdown events. This class will hold the event's details and provide a convenient getter for calculating the remaining duration.
import 'package:flutter/material.dart';
class CountdownEvent {
final String id;
final String title;
final DateTime targetDateTime;
bool notificationScheduled; // To track if notification is set for this event
CountdownEvent({
required this.id,
required this.title,
required this.targetDateTime,
this.notificationScheduled = false,
});
/// Calculates the duration remaining until the targetDateTime.
Duration get remainingDuration {
return targetDateTime.difference(DateTime.now());
}
/// Checks if the targetDateTime is in the past.
bool get isPast => remainingDuration.isNegative;
/// A simple factory method to create test events for demonstration.
factory CountdownEvent.createTestEvent(String id, String title, Duration offset) {
return CountdownEvent(
id: id,
title: title,
targetDateTime: DateTime.now().add(offset),
);
}
}
3. Notification Service (`notification_service.dart`)
This service class will encapsulate all logic related to initializing and scheduling local notifications. It uses flutter_local_notifications.
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:multi_countdown_app/countdown_event.dart';
class NotificationService {
static final FlutterLocalNotificationsPlugin _notificationsPlugin =
FlutterLocalNotificationsPlugin();
/// Initializes the notification plugin for both Android and iOS.
static Future initializeNotifications() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher'); // Your app icon
const DarwinInitializationSettings initializationSettingsIOS =
DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await _notificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse notificationResponse) async {
// Handle notification tap here.
// For example, navigate to a specific screen based on notificationResponse.payload.
debugPrint('Notification tapped with payload: ${notificationResponse.payload}');
},
);
}
/// Schedules a notification for an event if it has passed and a notification hasn't been sent yet.
static Future scheduleNotification(CountdownEvent event) async {
// Only schedule if the event is past and no notification has been marked as scheduled for it.
if (event.isPast && !event.notificationScheduled) {
final String formattedTime = DateFormat.yMMMd().add_jm().format(event.targetDateTime);
await _notificationsPlugin.show(
event.id.hashCode, // A unique integer ID for the notification
'${event.title} has arrived!',
'The event scheduled for "$formattedTime" is now!',
const NotificationDetails(
android: AndroidNotificationDetails(
'event_channel_id', // Unique channel ID
'Event Reminders', // Channel name
channelDescription: 'Notifications for upcoming and past events.',
importance: Importance.max,
priority: Priority.high,
showWhen: true,
),
iOS: DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
),
payload: event.id, // Optional payload to pass data when notification is tapped
);
// Mark the notification as scheduled to prevent duplicate alerts
event.notificationScheduled = true;
}
}
}
4. Multi Countdown Timer Widget (`multi_countdown_timer.dart`)
This is the main widget responsible for managing the list of events, updating their countdowns, and triggering notifications. It's a StatefulWidget because its UI changes over time.
import 'package:flutter/material.dart';
import 'dart:async'; // Required for Timer
import 'package:intl/intl.dart'; // Required for date formatting
import 'package:multi_countdown_app/countdown_event.dart';
import 'package:multi_countdown_app/notification_service.dart';
class MultiCountdownTimer extends StatefulWidget {
const MultiCountdownTimer({super.key});
@override
State createState() => _MultiCountdownTimerState();
}
class _MultiCountdownTimerState extends State {
late List _events;
Timer? _timer;
@override
void initState() {
super.initState();
// Initialize notification service
NotificationService.initializeNotifications();
// Populate with some test events.
// In a real app, these would come from a database or user input.
_events = [
CountdownEvent.createTestEvent('1', 'Project Deadline', const Duration(minutes: 5, seconds: 30)),
CountdownEvent.createTestEvent('2', 'Meeting Starts', const Duration(hours: 1, minutes: 15)),
CountdownEvent.createTestEvent('3', 'App Launch', const Duration(days: 3, hours: 2, minutes: 45)),
CountdownEvent.createTestEvent('4', 'Past Event (Test)', const Duration(seconds: -10)), // For immediate notification test
CountdownEvent.createTestEvent('5', 'Birthday Party', const Duration(days: 7, hours: 10)),
];
// Set up a periodic timer to update the UI and check for notifications every second.
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_updateTimers();
_checkAndNotify();
});
}
/// Triggers a UI rebuild to update all countdown timers.
void _updateTimers() {
setState(() {
// Calling setState rebuilds the UI, and each CountdownEvent's remainingDuration getter
// will be re-evaluated against the current time.
});
}
/// Iterates through events and schedules notifications for past events that haven't been notified.
void _checkAndNotify() {
for (var event in _events) {
NotificationService.scheduleNotification(event);
}
}
@override
void dispose() {
// Cancel the timer to prevent memory leaks when the widget is removed.
_timer?.cancel();
super.dispose();
}
/// Formats a Duration object into a human-readable string (e.g., "3d 05:12:45").
String _formatDuration(Duration duration) {
if (duration.isNegative) {
return "Event Passed!";
}
String twoDigits(int n) => n.toString().padLeft(2, '0');
String days = duration.inDays > 0 ? '${duration.inDays}d ' : '';
String hours = twoDigits(duration.inHours.remainder(24));
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
return '$days$hours:$minutes:$seconds';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Multi-Event Countdown'),
),
body: _events.isEmpty
? const Center(child: Text('No events to display.'))
: ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: _events.length,
itemBuilder: (context, index) {
final event = _events[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 8.0),
elevation: 4.0,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Target: ${DateFormat.yMMMd().add_jm().format(event.targetDateTime)}',
style: const TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: Text(
_formatDuration(event.remainingDuration),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: event.remainingDuration.isNegative
? Colors.red.shade700 // Red for past events
: Colors.blue.shade700, // Blue for future events
),
),
),
],
),
),
);
},
),
);
}
}
5. Main Application File (`main.dart`)
Finally, set up your main.dart to run the MultiCountdownTimer widget.
import 'package:flutter/material.dart';
import 'package:multi_countdown_app/multi_countdown_timer.dart';
void main() {
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,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const MultiCountdownTimer(),
);
}
}
Running the Application
To run this application, ensure you have set up your Flutter environment for Android and iOS development. For notifications to work correctly, you might need to build and run on a physical device or a well-configured emulator.
- For Android, no special permissions are needed in
AndroidManifest.xmlfor basic local notifications. - For iOS, ensure your project has the necessary capabilities for Push Notifications (though for local notifications,
flutter_local_notificationshandles most of it by requesting permissions).
Run your app:
flutter run
Conclusion
You have successfully built a multi-event countdown timer widget in Flutter, complete with local notification reminders. This solution demonstrates how to manage dynamic UI updates for multiple countdowns using a single periodic timer and how to integrate flutter_local_notifications to provide timely alerts to users.
This implementation can be further enhanced by:
- Allowing users to add, edit, or delete events.
- Persisting events using local storage (e.g.,
shared_preferencesor SQLite). - Implementing different types of notification reminders (e.g., 15 minutes before, 1 hour before).
- Adding more sophisticated UI elements and animations.
- Handling notifications when the app is in the background or terminated (which might require headless task setup for complex scenarios).
By understanding these core concepts, you can adapt and expand this widget to fit a wide range of application needs, providing a valuable feature to your users.