image

29 Mar 2026

9K

35K

Creating a Multi-Event Countdown Timer Widget with Reminder, Notification, and Repeat Features in Flutter

Countdown timers are a ubiquitous feature in modern applications, ranging from productivity tools and event organizers to e-commerce platforms and gaming. Building a robust countdown timer in Flutter that can handle multiple events, provide timely reminders, send notifications, and even repeat on a schedule, presents an interesting challenge. This article delves into the architecture and implementation details for creating such a sophisticated widget.

Key Features of Our Advanced Countdown Timer

  • Multi-Event Management: Ability to display and manage multiple distinct countdown events simultaneously.
  • Countdown Timer Logic: Accurate real-time countdown display for each event, showing remaining days, hours, minutes, and seconds.
  • Reminder Mechanism: Option to set reminders a certain period before the event starts.
  • Notification System: Leveraging local notifications to alert users when an event is approaching or has started.
  • Repeat Feature: Functionality to automatically reschedule events daily, weekly, monthly, or yearly.

Prerequisites

Before diving into the implementation, ensure you have a basic understanding of Flutter development and have the following packages added to your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  flutter_local_notifications: ^17.0.0 # For local notifications
  intl: ^0.19.0 # For date and time formatting (optional but recommended)

After adding, run flutter pub get to fetch the packages.

Step-by-Step Implementation

1. Defining the Event Model

First, we need a data model to represent each countdown event. This model will hold the event's name, target date/time, repeat type, and reminder settings.


import 'package:flutter/foundation.dart';

enum RepeatType {
  none,
  daily,
  weekly,
  monthly,
  yearly,
}

class CountdownEvent {
  final String id;
  String title;
  DateTime eventDateTime;
  RepeatType repeatType;
  Duration? reminderDuration; // e.g., 15 minutes before

  CountdownEvent({
    required this.id,
    required this.title,
    required this.eventDateTime,
    this.repeatType = RepeatType.none,
    this.reminderDuration,
  });

  // Helper to update event for repeat
  void rescheduleEvent() {
    DateTime now = DateTime.now();
    DateTime newDateTime = eventDateTime;

    while (newDateTime.isBefore(now)) {
      switch (repeatType) {
        case RepeatType.daily:
          newDateTime = newDateTime.add(const Duration(days: 1));
          break;
        case RepeatType.weekly:
          newDateTime = newDateTime.add(const Duration(days: 7));
          break;
        case RepeatType.monthly:
          // A bit more complex for monthly to handle different day counts
          newDateTime = DateTime(newDateTime.year, newDateTime.month + 1, newDateTime.day,
              newDateTime.hour, newDateTime.minute, newDateTime.second);
          break;
        case RepeatType.yearly:
          newDateTime = DateTime(newDateTime.year + 1, newDateTime.month, newDateTime.day,
              newDateTime.hour, newDateTime.minute, newDateTime.second);
          break;
        case RepeatType.none:
          break;
      }
      if (repeatType == RepeatType.none) break; // Exit if not repeating
    }
    eventDateTime = newDateTime;
  }
}

2. Setting Up Local Notifications

Initialize flutter_local_notifications early in your application lifecycle, typically in your main.dart or a dedicated notification service.


import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future initializeNotifications() async {
  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 response) async {
      // Handle notification tap
      if (response.payload != null) {
        debugPrint('notification payload: ${response.payload}');
      }
    },
  );
}

// Call this in your main function before runApp:
// void main() async {
//   WidgetsFlutterBinding.ensureInitialized();
//   await initializeNotifications();
//   runApp(const MyApp());
// }

3. Designing the Multi-Event Countdown Screen

We'll create a stateful widget to manage a list of CountdownEvent objects. This widget will display each event using a dedicated sub-widget.


import 'dart:async';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; // For formatting duration nicely

// Assuming CountdownEvent class is defined in event_model.dart
// And initializeNotifications is defined in notification_service.dart

class MultiEventCountdownScreen extends StatefulWidget {
  const MultiEventCountdownScreen({super.key});

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

class _MultiEventCountdownScreenState extends State {
  final List _events = [];

  @override
  void initState() {
    super.initState();
    _loadEvents(); // Simulate loading events
  }

  void _loadEvents() {
    setState(() {
      _events.add(CountdownEvent(
        id: '1',
        title: 'Project Deadline',
        eventDateTime: DateTime.now().add(const Duration(days: 5, hours: 10, minutes: 30)),
        reminderDuration: const Duration(hours: 1),
        repeatType: RepeatType.none,
      ));
      _events.add(CountdownEvent(
        id: '2',
        title: 'Daily Standup',
        eventDateTime: DateTime.now().add(const Duration(minutes: 2)), // Soon for testing repeat
        reminderDuration: const Duration(minutes: 5),
        repeatType: RepeatType.daily,
      ));
      _events.add(CountdownEvent(
        id: '3',
        title: 'Yearly Anniversary',
        eventDateTime: DateTime(DateTime.now().year + 1, 1, 1),
        reminderDuration: const Duration(days: 7),
        repeatType: RepeatType.yearly,
      ));

      // Schedule notifications for initial load
      for (var event in _events) {
        _scheduleEventNotifications(event);
      }
    });
  }

  void _addEvent(CountdownEvent newEvent) {
    setState(() {
      _events.add(newEvent);
      _scheduleEventNotifications(newEvent);
    });
  }

  void _updateEvent(CountdownEvent updatedEvent) {
    setState(() {
      final index = _events.indexWhere((event) => event.id == updatedEvent.id);
      if (index != -1) {
        _events[index] = updatedEvent;
        // Re-schedule notifications if event details changed
        _cancelEventNotifications(updatedEvent.id);
        _scheduleEventNotifications(updatedEvent);
      }
    });
  }

  void _deleteEvent(String eventId) {
    setState(() {
      _events.removeWhere((event) => event.id == eventId);
      _cancelEventNotifications(eventId);
    });
  }

  // --- Notification Scheduling Helper ---
  Future _scheduleEventNotifications(CountdownEvent event) async {
    // Unique ID for the event's primary notification
    int eventNotificationId = int.parse(event.id); 
    // Unique ID for the event's reminder notification (e.g., event ID + 1000)
    int reminderNotificationId = int.parse(event.id) + 1000; 

    // Cancel any existing notifications for this event before rescheduling
    await flutterLocalNotificationsPlugin.cancel(eventNotificationId);
    await flutterLocalNotificationsPlugin.cancel(reminderNotificationId);

    // Schedule primary event notification
    if (event.eventDateTime.isAfter(DateTime.now())) {
      await _scheduleNotification(
        id: eventNotificationId,
        title: '${event.title} is starting!',
        body: 'The event "${event.title}" is happening now.',
        scheduledDate: event.eventDateTime,
        payload: event.id,
      );
    }

    // Schedule reminder notification if set
    if (event.reminderDuration != null) {
      final reminderTime = event.eventDateTime.subtract(event.reminderDuration!);
      if (reminderTime.isAfter(DateTime.now())) {
        await _scheduleNotification(
          id: reminderNotificationId,
          title: 'Reminder: ${event.title} is coming soon!',
          body: 'Your event "${event.title}" is in '
                '${event.reminderDuration!.inMinutes} minutes.',
          scheduledDate: reminderTime,
          payload: '${event.id}_reminder',
        );
      }
    }
  }

  Future _cancelEventNotifications(String eventId) async {
    await flutterLocalNotificationsPlugin.cancel(int.parse(eventId));
    await flutterLocalNotificationsPlugin.cancel(int.parse(eventId) + 1000); // Also cancel reminder
  }

  Future _scheduleNotification({
    required int id,
    required String title,
    required String body,
    required DateTime scheduledDate,
    String? payload,
  }) async {
    if (scheduledDate.isBefore(DateTime.now())) return; // Don't schedule past events

    const AndroidNotificationDetails androidPlatformChannelSpecifics =
        AndroidNotificationDetails(
      'countdown_timer_channel',
      'Countdown Timer Notifications',
      channelDescription: 'Notifications for your countdown events',
      importance: Importance.high,
      priority: Priority.high,
      ticker: 'ticker',
    );

    const DarwinNotificationDetails iOSPlatformChannelSpecifics =
        DarwinNotificationDetails();

    const NotificationDetails platformChannelSpecifics = NotificationDetails(
      android: androidPlatformChannelSpecifics,
      iOS: iOSPlatformChannelSpecifics,
    );

    await flutterLocalNotificationsPlugin.schedule(
      id,
      title,
      body,
      scheduledDate,
      platformChannelSpecifics,
      payload: payload,
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Multi-Event Countdown'),
      ),
      body: _events.isEmpty
          ? const Center(child: Text('No events yet! Add one below.'))
          : ListView.builder(
              itemCount: _events.length,
              itemBuilder: (context, index) {
                final event = _events[index];
                return CountdownTile(
                  event: event,
                  onEventComplete: (completedEvent) {
                    // Handle event completion, e.g., reschedule or remove
                    if (completedEvent.repeatType != RepeatType.none) {
                      completedEvent.rescheduleEvent();
                      _updateEvent(completedEvent); // This will also reschedule notifications
                    } else {
                      _deleteEvent(completedEvent.id);
                    }
                  },
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // TODO: Implement an Add Event form/dialog
          // For now, adding a dummy event
          _addEvent(CountdownEvent(
            id: DateTime.now().millisecondsSinceEpoch.toString(), // Unique ID
            title: 'New Event ${_events.length + 1}',
            eventDateTime: DateTime.now().add(const Duration(minutes: 10)),
            reminderDuration: const Duration(minutes: 2),
            repeatType: RepeatType.none,
          ));
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

4. Implementing the Individual Countdown Tile

Each event needs its own widget to display the countdown. This widget will be responsible for starting its own timer and updating its display.


class CountdownTile extends StatefulWidget {
  final CountdownEvent event;
  final Function(CountdownEvent) onEventComplete;

  const CountdownTile({
    super.key,
    required this.event,
    required this.onEventComplete,
  });

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

class _CountdownTileState extends State {
  Timer? _timer;
  Duration _timeRemaining = Duration.zero;

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

  @override
  void didUpdateWidget(covariant CountdownTile oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.event.eventDateTime != oldWidget.event.eventDateTime) {
      _stopTimer();
      _startTimer();
    }
  }

  void _startTimer() {
    _timeRemaining = widget.event.eventDateTime.difference(DateTime.now());

    if (_timeRemaining.isNegative) {
      _timeRemaining = Duration.zero;
      WidgetsBinding.instance.addPostFrameCallback((_) {
        widget.onEventComplete(widget.event);
      });
      return;
    }

    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        _timeRemaining = widget.event.eventDateTime.difference(DateTime.now());
        if (_timeRemaining.isNegative) {
          _timeRemaining = Duration.zero;
          _stopTimer();
          widget.onEventComplete(widget.event); // Notify parent event is complete
        }
      });
    });
  }

  void _stopTimer() {
    _timer?.cancel();
    _timer = null;
  }

  @override
  void dispose() {
    _stopTimer();
    super.dispose();
  }

  String _formatDuration(Duration duration) {
    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 Card(
      margin: const EdgeInsets.all(8.0),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              widget.event.title,
              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text(
              'Target: ${DateFormat('EEE, MMM d, y HH:mm').format(widget.event.eventDateTime)}',
              style: const TextStyle(fontSize: 14, color: Colors.grey),
            ),
            if (widget.event.repeatType != RepeatType.none)
              Text(
                'Repeats: ${widget.event.repeatType.name.toUpperCase()}',
                style: const TextStyle(fontSize: 14, color: Colors.blueAccent),
              ),
            const SizedBox(height: 8),
            _timeRemaining.inSeconds <= 0
                ? const Text(
                    'Event has started!',
                    style: TextStyle(fontSize: 24, color: Colors.green, fontWeight: FontWeight.bold),
                  )
                : Text(
                    _formatDuration(_timeRemaining),
                    style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  ),
          ],
        ),
      ),
    );
  }
}

5. Integrating Reminders and Notifications (Already Handled)

The logic for scheduling notifications, including reminders, is integrated directly into the _MultiEventCountdownScreenState. Whenever an event is added or updated (especially its date or repeat type), the _scheduleEventNotifications method is called. This method ensures that appropriate notifications are set up for both the event's start time and its reminder time.

6. Adding the Repeat Feature (Already Handled)

The rescheduleEvent() method within the CountdownEvent class handles the core logic for repeating events. When an event completes and has a repeatType other than none, the onEventComplete callback in MultiEventCountdownScreen triggers this method. The parent widget then updates the event in its list, which in turn causes the CountdownTile to rebuild and restart its timer with the new event date, and the notifications are also automatically re-scheduled.

7. Managing Event State

In this example, we use a simple setState in the parent _MultiEventCountdownScreenState to manage the list of events. For larger applications, you might consider more advanced state management solutions like Provider, BLoC, Riverpod, or GetX to handle adding, updating, and deleting events more robustly, especially if events need to be persisted across app launches (e.g., using shared_preferences or a database like Hive/SQLite).

Conclusion

Building a multi-event countdown timer with reminders, notifications, and repeat features in Flutter requires careful orchestration of UI updates, background task scheduling (via notifications), and robust data modeling. By breaking down the problem into smaller, manageable components—an event model, a notification service, a main event list manager, and individual countdown tiles—we can construct a powerful and user-friendly solution. This foundation can be further extended with persistence, event editing forms, and more sophisticated repeat patterns to create a truly comprehensive event management system.

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