Building an Event Countdown Timer Widget with Notification Reminders in Flutter
Creating engaging and functional applications often involves interactive elements that keep users informed and engaged. A common requirement for event-based applications is a countdown timer that not only displays the remaining time but also provides timely reminders. This article will guide you through building a robust Flutter widget that serves as an event countdown timer, integrated with local notification reminders.
Prerequisites
- Basic understanding of Flutter development.
- Familiarity with Dart programming.
1. Setting Up the Project and Dependencies
First, create a new Flutter project if you haven't already. We'll need the flutter_local_notifications package to handle local notifications.
flutter create event_countdown_app
cd event_countdown_app
Add the following dependency to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
flutter_local_notifications: ^17.1.0
timezone: ^0.9.2 # Required for scheduling notifications in specific timezones
rxdart: ^0.28.0 # Optional, for notification stream listeners
Run flutter pub get to fetch the new dependencies.
Platform-Specific Setup for Notifications:
- Android: No special setup required beyond the dependency.
- iOS: Add the following to your
ios/Runner/AppDelegate.swiftfile (if using Swift):
import UIKit
import Flutter
import flutter_local_notifications
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
FlutterLocalNotificationsPlugin.set ';application:didFinishLaunchingWithOptions:' with: self
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
2. Event Data Model
Let's define a simple data model for our event. This will include the event title and its scheduled date/time.
import 'package:flutter/foundation.dart';
class Event {
final String id;
final String title;
final DateTime eventDateTime;
Event({
required this.id,
required this.title,
required this.eventDateTime,
});
}
3. Notification Service
This service will handle the initialization of flutter_local_notifications and the scheduling of reminders.
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tzdata;
import 'package:rxdart/rxdart.dart'; // For listening to notifications
class NotificationService {
static final _notifications = FlutterLocalNotificationsPlugin();
static final onNotifications = BehaviorSubject<String?>();
static Future init({bool initScheduled = false}) async {
tzdata.initializeUsaAndCanada(); // Or any other region relevant to your users
tz.setLocalLocation(tz.getLocation('America/New_York')); // Set your local timezone
const android = AndroidInitializationSettings('@mipmap/ic_launcher');
const iOS = DarwinInitializationSettings();
const settings = InitializationSettings(android: android, iOS: iOS);
await _notifications.initialize(
settings,
onDidReceiveNotificationResponse: (notificationResponse) async {
onNotifications.add(notificationResponse.payload);
},
);
}
static Future _notificationDetails() async {
return const NotificationDetails(
android: AndroidNotificationDetails(
'channel id',
'channel name',
channelDescription: 'channel description',
importance: Importance.max,
priority: Priority.high,
ticker: 'ticker',
),
iOS: DarwinNotificationDetails(),
);
}
static Future showNotification({
int id = 0,
String? title,
String? body,
String? payload,
}) async =>
_notifications.show(
id,
title,
body,
await _notificationDetails(),
payload: payload,
);
static Future scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledDate,
String? payload,
}) async {
await _notifications.zonedSchedule(
id,
title,
body,
tz.TZDateTime.from(scheduledDate, tz.local),
await _notificationDetails(),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.dateAndTime,
payload: payload,
);
}
static void cancelNotification(int id) async {
await _notifications.cancel(id);
}
static void cancelAllNotifications() async {
await _notifications.cancelAll();
}
}
4. Countdown Timer Widget
This widget will display the remaining time until the event. It will update every second.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:event_countdown_app/event_model.dart';
class CountdownTimerWidget extends StatefulWidget {
final Event event;
const CountdownTimerWidget({Key? key, required this.event}) : super(key: key);
@override
_CountdownTimerWidgetState createState() => _CountdownTimerWidgetState();
}
class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
late Timer _timer;
Duration _remainingDuration = Duration.zero;
@override
void initState() {
super.initState();
_startTimer();
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
void _startTimer() {
_updateRemainingDuration(); // Initial update
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted) {
_updateRemainingDuration();
}
});
}
void _updateRemainingDuration() {
final now = DateTime.now();
setState(() {
_remainingDuration = widget.event.eventDateTime.difference(now);
});
}
String _formatDuration(Duration duration) {
if (duration.isNegative) {
return "Event passed!";
}
String twoDigits(int n) => n.toString().padLeft(2, '0');
String days = duration.inDays.toString();
String hours = twoDigits(duration.inHours.remainder(24));
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
return "${days}d ${hours}h ${minutes}m ${seconds}s";
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.event.title,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Text(
_formatDuration(_remainingDuration),
style: const TextStyle(fontSize: 36, color: Colors.blueAccent),
),
],
);
}
}
5. Integrating It All in main.dart
Now, let's put everything together. We'll initialize the notification service, define an event, and schedule reminders for it.
import 'package:flutter/material.dart';
import 'package:event_countdown_app/event_model.dart';
import 'package:event_countdown_app/countdown_timer_widget.dart';
import 'package:event_countdown_app/notification_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await NotificationService.init(initScheduled: true);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Event Countdown',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const EventCountdownScreen(),
);
}
}
class EventCountdownScreen extends StatefulWidget {
const EventCountdownScreen({Key? key}) : super(key: key);
@override
State<EventCountdownScreen> createState() => _EventCountdownScreenState();
}
class _EventCountdownScreenState extends State<EventCountdownScreen> {
final Event upcomingEvent = Event(
id: '1',
title: 'Flutter Connect Conference',
eventDateTime: DateTime.now().add(const Duration(days: 7, hours: 10, minutes: 30)),
);
@override
void initState() {
super.initState();
_scheduleEventReminders();
_listenToNotifications();
}
void _scheduleEventReminders() {
// Schedule a reminder 24 hours before
final reminder1Day = upcomingEvent.eventDateTime.subtract(const Duration(days: 1));
if (reminder1Day.isAfter(DateTime.now())) {
NotificationService.scheduleNotification(
id: int.parse(upcomingEvent.id), // Unique ID for the event
title: 'Reminder: ${upcomingEvent.title}',
body: 'Your event is starting in 1 day!',
scheduledDate: reminder1Day,
payload: 'event_reminder_1d_${upcomingEvent.id}',
);
}
// Schedule a reminder 1 hour before
final reminder1Hour = upcomingEvent.eventDateTime.subtract(const Duration(hours: 1));
if (reminder1Hour.isAfter(DateTime.now())) {
NotificationService.scheduleNotification(
id: int.parse(upcomingEvent.id) + 1, // Another unique ID for this reminder
title: 'Reminder: ${upcomingEvent.title}',
body: 'Your event is starting in 1 hour!',
scheduledDate: reminder1Hour,
payload: 'event_reminder_1h_${upcomingEvent.id}',
);
}
}
void _listenToNotifications() {
NotificationService.onNotifications.listen((payload) {
if (payload != null) {
// Handle notification tap, e.g., navigate to event details screen
print('Notification tapped with payload: $payload');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Notification tapped! Payload: $payload')),
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Event Countdown Timer'),
),
body: Center(
child: CountdownTimerWidget(event: upcomingEvent),
),
);
}
}
Explanation of Key Components:
EventModel: A simple class to hold event details like title and its specificDateTime.NotificationService:- Initializes
FlutterLocalNotificationsPluginwith platform-specific settings. tzdata.initializeUsaAndCanada()andtz.setLocalLocation()are crucial for scheduling notifications that respect the device's timezone, preventing issues with daylight saving or timezone changes.scheduleNotificationuses_notifications.zonedSchedule()to set a precise notification for a future date and time.onNotificationsis aBehaviorSubjectfromrxdartthat allows listening to notification taps from anywhere in the app, facilitating navigation or specific actions upon interaction.
- Initializes
CountdownTimerWidget:- This is a
StatefulWidgetthat holds aTimer. - The
_timeris set to update the_remainingDurationevery second. _updateRemainingDuration()calculates the difference between the current time and the event time._formatDuration()converts theDurationobject into a human-readable string (e.g., "7d 10h 30m 00s").dispose()ensures the timer is cancelled to prevent memory leaks when the widget is removed from the widget tree.
- This is a
EventCountdownScreen:- In
initState, it calls_scheduleEventReminders()to set up notifications for theupcomingEvent. We've added two example reminders: one day and one hour before the event. - It also calls
_listenToNotifications()to react when a user taps on a notification. - The screen displays the
CountdownTimerWidgetfor the defined event.
- In
Conclusion
You have now successfully built a Flutter application that displays a dynamic countdown timer for an event and schedules local notification reminders for that event. This solution provides a robust foundation that can be extended to support multiple events, custom reminder intervals, persistent storage for events, and more sophisticated UI designs. Remember to thoroughly test notifications on both Android and iOS devices, especially concerning exact scheduling and background execution.