image

04 Mar 2026

9K

35K

Building an Event Countdown Widget with Notification Reminder in Flutter

In today's fast-paced world, keeping track of important events is crucial. Whether it's a birthday, an anniversary, a project deadline, or a public holiday, a clear visual countdown coupled with timely reminders can significantly enhance user experience and productivity. This article will guide you through building a dynamic event countdown widget in Flutter, complete with local notification reminders, ensuring your users never miss an important date.

Prerequisites

Before we dive into the implementation, ensure you have the following:

  • Flutter SDK installed and configured.
  • Basic understanding of Dart programming and Flutter widgets.
  • A code editor (VS Code, Android Studio) with Flutter and Dart plugins.

Project Setup and Dependencies

First, create a new Flutter project or open an existing one. We'll need to add a few packages to our pubspec.yaml file:

  • flutter_local_notifications: For scheduling and displaying local notifications.
  • intl: For easy date and time formatting.
  • timezone: This is a dependency for flutter_local_notifications's advanced scheduling features, required for zonedSchedule.

Update your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  flutter_local_notifications: ^17.1.0
  intl: ^0.19.0
  timezone: ^0.9.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

After modifying pubspec.yaml, run flutter pub get in your terminal to fetch the new packages.

Designing the Countdown Widget

Our countdown widget will be a StatefulWidget that updates every second to display the remaining time until a target event. We'll use a Timer for this purpose.

Create a new file, e.g., lib/countdown_widget.dart:


import 'dart:async';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

class CountdownWidget extends StatefulWidget {
  final DateTime eventDateTime;
  final String eventName;

  const CountdownWidget({
    Key? key,
    required this.eventDateTime,
    required this.eventName,
  }) : super(key: key);

  @override
  _CountdownWidgetState createState() => _CountdownWidgetState();
}

class _CountdownWidgetState extends State {
  late Timer _timer;
  Duration _timeRemaining = Duration.zero;

  @override
  void initState() {
    super.initState();
    _startCountdown();
  }

  void _startCountdown() {
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      final now = DateTime.now();
      setState(() {
        if (widget.eventDateTime.isAfter(now)) {
          _timeRemaining = widget.eventDateTime.difference(now);
        } else {
          _timeRemaining = Duration.zero;
          _timer.cancel(); // Event has passed
        }
      });
    });
  }

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  String _formatDuration(Duration duration) {
    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 days, $hours hours, $minutes minutes, $seconds seconds';
  }

  @override
  Widget build(BuildContext context) {
    final bool eventPassed = _timeRemaining.inSeconds <= 0;

    return Card(
      margin: const EdgeInsets.all(16.0),
      elevation: 4,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
      child: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              widget.eventName,
              style: const TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
                color: Colors.blueAccent,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 10),
            Text(
              'Scheduled for: ${DateFormat('EEE, MMM d, yyyy h:mm a').format(widget.eventDateTime)}',
              style: TextStyle(
                fontSize: 16,
                fontStyle: FontStyle.italic,
                color: Colors.grey[600],
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 20),
            if (eventPassed)
              const Text(
                'Event has passed!',
                style: TextStyle(
                  fontSize: 22,
                  fontWeight: FontWeight.bold,
                  color: Colors.red,
                ),
              )
            else
              Text(
                _formatDuration(_timeRemaining),
                style: const TextStyle(
                  fontSize: 28,
                  fontWeight: FontWeight.w900,
                  color: Colors.deepPurple,
                ),
                textAlign: TextAlign.center,
              ),
          ],
        ),
      ),
    );
  }
}

Implementing Local Notifications

The flutter_local_notifications package requires some platform-specific setup. We'll also create a dedicated service to handle notification initialization and scheduling.

Android Configuration

In android/app/src/main/AndroidManifest.xml, ensure you have the following permission (usually present):


<manifest ...>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.VIBRATE"/>
    <!-- For Android 13+ -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    ...
</manifest>

Also, add the following inside the <application> tag:


<application ...>
    <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED"/>
            <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
            <action android:name="android.intent.action.QUICKBOOT_POWERON" />
            <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
        </intent-filter>
    </receiver>
    <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
    ...
</application>

For Android 12+, if targeting SDK 31+, you might need to specify android:exported="true" for your main activity in AndroidManifest.xml if you encounter issues. The plugin usually handles this, but it's good to be aware.

iOS Configuration

In ios/Runner/AppDelegate.swift, add the following lines:


import UIKit
import Flutter
import flutter_local_notifications // Import the plugin

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    // Add the following lines:
    FlutterLocalNotificationsPlugin.set 

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

class NotificationService {
  static final FlutterLocalNotificationsPlugin _notificationsPlugin =
      FlutterLocalNotificationsPlugin();

  static Future init() async {
    tz.initializeTimeZones();

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

    final DarwinInitializationSettings initializationSettingsDarwin =
        DarwinInitializationSettings(
      onDidReceiveLocalNotification: (id, title, body, payload) async {
        // Handle notification tapped when app is in foreground (iOS only)
      },
    );

    final InitializationSettings initializationSettings = InitializationSettings(
      android: initializationSettingsAndroid,
      iOS: initializationSettingsDarwin,
    );

    await _notificationsPlugin.initialize(
      initializationSettings,
      onDidReceiveNotificationResponse: (notificationResponse) async {
        // Handle notification tapped when app is in background or terminated
        // You can use notificationResponse.payload to navigate or perform actions
      },
    );
  }

  static Future scheduleNotification({
    required int id,
    required String title,
    required String body,
    required DateTime scheduledDateTime,
    String? payload,
  }) async {
    await _notificationsPlugin.zonedSchedule(
      id,
      title,
      body,
      tz.TZDateTime.from(scheduledDateTime, tz.local),
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'event_countdown_channel', // id
          'Event Countdown Notifications', // name
          channelDescription: 'Notifications for upcoming events',
          importance: Importance.max,
          priority: Priority.high,
          ticker: 'ticker',
        ),
        iOS: DarwinNotificationDetails(),
      ),
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: null, // Schedule for exact date/time
      payload: payload,
    );
  }

  static Future cancelAllNotifications() async {
    await _notificationsPlugin.cancelAll();
  }
}

Integrating Countdown and Notifications

Now, let's put it all together in our main application file (lib/main.dart). We'll initialize the notification service and then schedule a notification for our event.


import 'package:flutter/material.dart';
import 'package:flutter_app/countdown_widget.dart'; // Make sure this path is correct
import 'package:flutter_app/notification_service.dart'; // Make sure this path is correct

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await NotificationService.init(); // Initialize notification service
  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 createState() => _EventCountdownScreenState();
}

class _EventCountdownScreenState extends State {
  // Example event
  final DateTime _myEventDate = DateTime.now().add(const Duration(days: 3, hours: 5, minutes: 30));
  final String _myEventName = 'Project Deadline Submission';

  @override
  void initState() {
    super.initState();
    _scheduleEventReminder();
  }

  void _scheduleEventReminder() {
    // Schedule a reminder 1 hour before the event
    final DateTime reminderTime = _myEventDate.subtract(const Duration(hours: 1));

    // Ensure the reminder is not scheduled for a time in the past
    if (reminderTime.isAfter(DateTime.now())) {
      NotificationService.scheduleNotification(
        id: 0, // Unique ID for this notification
        title: 'Reminder: $_myEventName',
        body: 'Your event is in 1 hour!',
        scheduledDateTime: reminderTime,
        payload: 'project_deadline',
      );
      print('Notification scheduled for: $reminderTime');
    } else {
      print('Reminder time is in the past, not scheduling notification.');
    }

    // Schedule another reminder 1 day before the event
    final DateTime dayBeforeReminderTime = _myEventDate.subtract(const Duration(days: 1));
    if (dayBeforeReminderTime.isAfter(DateTime.now())) {
      NotificationService.scheduleNotification(
        id: 1, // Another unique ID
        title: 'Upcoming: $_myEventName',
        body: 'Your event is tomorrow!',
        scheduledDateTime: dayBeforeReminderTime,
        payload: 'project_deadline_day_before',
      );
      print('Day before notification scheduled for: $dayBeforeReminderTime');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Event Countdown & Reminder'),
      ),
      body: Center(
        child: SingleChildScrollView(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CountdownWidget(
                eventDateTime: _myEventDate,
                eventName: _myEventName,
              ),
              const SizedBox(height: 30),
              ElevatedButton(
                onPressed: () {
                  NotificationService.cancelAllNotifications();
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('All notifications cancelled!')),
                  );
                },
                child: const Text('Cancel All Notifications'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Enhancements and Further Considerations

  • Multiple Events: To manage multiple events, you would typically store them in a list (e.g., using a List<Event>) and generate multiple CountdownWidget instances. Each notification would need a unique ID.
  • Persistent Storage: For real applications, event data and scheduled notifications should persist across app restarts. Consider using:
    • shared_preferences for simple key-value pairs.
    • Hive for a lightweight NoSQL database.
    • sqflite for a more structured SQL database.
    When the app starts, load events from storage and reschedule their notifications.
  • User Input for Events: Implement a form where users can add new events, specify the date/time, and choose reminder intervals (e.g., 1 hour before, 1 day before).
  • Time Zones: The timezone package is crucial for scheduling notifications accurately regardless of the device's time zone settings or if the event is in a different time zone. We've used tz.local for simplicity, but you can specify any IANA time zone identifier.
  • Custom Notification Details: Customize notification sounds, icons, and behavior (e.g., repeating notifications) using the NotificationDetails object.
  • Foreground Notification Handling: The onDidReceiveNotificationResponse callback in _notificationsPlugin.initialize is vital for handling user taps on notifications when the app is in the background or terminated. For iOS foreground notifications, use onDidReceiveLocalNotification.

Conclusion

You've successfully built an event countdown widget in Flutter that not only visually tracks time remaining but also provides timely notification reminders. This foundation can be extended with persistent storage, user-defined events, and more advanced notification features to create a robust and highly functional reminder application. By combining Flutter's rich UI capabilities with powerful notification plugins, you can significantly improve user engagement and ensure important moments are never overlooked.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is