image

14 May 2026

9K

35K

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

Countdown timers are essential in many applications, from productivity tools to event management. A simple countdown might suffice for a single event, but a robust multi-event countdown timer, enriched with features like reminders, notifications, repeat functionality, and custom labels, elevates the user experience significantly. This article explores the architecture and implementation of such a sophisticated widget in Flutter, empowering developers to create highly interactive and feature-rich applications.

The Core Concept: Managing Multiple Timers

At its heart, a multi-event countdown timer needs to manage several independent countdowns concurrently. Each event will have a specific target date and time, a unique identifier, and various associated properties. Flutter's reactive nature and powerful state management options make this task manageable and efficient.

Key Features Breakdown

1. Custom Labels

Allowing users to assign custom names or descriptions to each event is fundamental. This makes the timer highly personal and easy to understand at a glance, transforming a generic countdown into "Project Deadline," "Birthday Party," or "Meeting Start."

2. Countdown Logic

The core functionality involves calculating the remaining duration (days, hours, minutes, seconds) until each event's target time. This calculation needs to be updated in real-time, typically every second, to provide an accurate countdown display.

3. Reminders and Notifications

Beyond a visual countdown, proactive alerts are crucial. This feature involves scheduling local notifications that trigger at a specified time before an event occurs (e.g., 15 minutes before, 1 hour before). For repeating events, these notifications must be intelligently rescheduled.

4. Repeat Functionality

Many events are recurring (daily meetings, weekly tasks, annual anniversaries). Implementing a repeat option (daily, weekly, monthly, annually, or custom intervals) simplifies event management by automatically advancing the target date/time after an event concludes.

Architectural Considerations

To build a robust multi-event countdown timer, a well-thought-out architecture is vital. We will focus on:

  • Data Model: A structured class to represent each event.
  • State Management: A mechanism to manage the list of events and their real-time countdowns. Provider is an excellent choice for this.
  • Local Notifications: Utilizing a plugin to handle scheduling and displaying notifications.

Implementation Steps

1. Project Setup and Dependencies

First, create a new Flutter project and add the necessary dependencies in your pubspec.yaml file:


dependencies:
  flutter:
    sdk: flutter
  provider: ^6.0.5 # For state management
  flutter_local_notifications: ^17.0.0 # For local notifications
  timezone: ^0.9.2 # For local notifications time zone handling

Run flutter pub get after adding dependencies.

2. The Event Data Model

Define an Event class to hold all the necessary properties for each countdown item. This class should be immutable or have methods for updating properties.


import 'package:flutter/foundation.dart';

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

class Event {
  final String id;
  String name;
  DateTime targetDateTime;
  Duration? reminderOffset; // e.g., 15 minutes before
  bool enableNotifications;
  RepeatInterval repeatInterval;
  int notificationId; // Unique ID for flutter_local_notifications

  Event({
    required this.id,
    required this.name,
    required this.targetDateTime,
    this.reminderOffset,
    this.enableNotifications = true,
    this.repeatInterval = RepeatInterval.none,
    required this.notificationId,
  });

  Event copyWith({
    String? id,
    String? name,
    DateTime? targetDateTime,
    Duration? reminderOffset,
    bool? enableNotifications,
    RepeatInterval? repeatInterval,
    int? notificationId,
  }) {
    return Event(
      id: id ?? this.id,
      name: name ?? this.name,
      targetDateTime: targetDateTime ?? this.targetDateTime,
      reminderOffset: reminderOffset ?? this.reminderOffset,
      enableNotifications: enableNotifications ?? this.enableNotifications,
      repeatInterval: repeatInterval ?? this.repeatInterval,
      notificationId: notificationId ?? this.notificationId,
    );
  }

  // Helper to advance repeating events
  void advanceRepeat() {
    switch (repeatInterval) {
      case RepeatInterval.daily:
        targetDateTime = targetDateTime.add(const Duration(days: 1));
        break;
      case RepeatInterval.weekly:
        targetDateTime = targetDateTime.add(const Duration(days: 7));
        break;
      case RepeatInterval.monthly:
        targetDateTime = DateTime(targetDateTime.year, targetDateTime.month + 1, targetDateTime.day,
            targetDateTime.hour, targetDateTime.minute, targetDateTime.second);
        break;
      case RepeatInterval.yearly:
        targetDateTime = DateTime(targetDateTime.year + 1, targetDateTime.month, targetDateTime.day,
            targetDateTime.hour, targetDateTime.minute, targetDateTime.second);
        break;
      case RepeatInterval.none:
        // Do nothing
        break;
    }
  }
}

3. State Management with Provider (CountdownProvider)

Create a ChangeNotifier to manage the list of events. This provider will handle adding, updating, deleting events, and also the core countdown logic.


import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart'; // Add uuid: ^4.3.3 to pubspec.yaml for unique IDs
import 'package:your_app_name/models/event.dart'; // Adjust import path
import 'package:your_app_name/services/notification_service.dart'; // We'll create this next

class CountdownProvider with ChangeNotifier {
  List<Event> _events = [];
  Timer? _timer;
  final Uuid _uuid = const Uuid();
  final NotificationService _notificationService = NotificationService();

  List<Event> get events => _events;

  CountdownProvider() {
    _initNotifications();
    _startTimer();
  }

  void _initNotifications() async {
    await _notificationService.init();
  }

  void _startTimer() {
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      _updateEventStatuses();
      notifyListeners();
    });
  }

  void _updateEventStatuses() {
    for (var i = 0; i < _events.length; i++) {
      final event = _events[i];
      if (event.targetDateTime.isBefore(DateTime.now())) {
        // Event has passed
        if (event.repeatInterval != RepeatInterval.none) {
          event.advanceRepeat(); // Advance to the next occurrence
          _scheduleEventNotification(event); // Reschedule notification
        } else {
          // Remove non-repeating, past events or mark as completed
          // For simplicity, we'll keep them visible but could remove them
        }
      }
    }
  }

  void addEvent(Event event) {
    _events.add(event);
    _scheduleEventNotification(event);
    notifyListeners();
  }

  void updateEvent(Event updatedEvent) {
    final index = _events.indexWhere((event) => event.id == updatedEvent.id);
    if (index != -1) {
      _notificationService.cancelNotification(updatedEvent.notificationId); // Cancel old notification
      _events[index] = updatedEvent;
      _scheduleEventNotification(updatedEvent); // Schedule new notification
      notifyListeners();
    }
  }

  void deleteEvent(String id) {
    final event = _events.firstWhere((e) => e.id == id);
    _notificationService.cancelNotification(event.notificationId);
    _events.removeWhere((event) => event.id == id);
    notifyListeners();
  }

  void _scheduleEventNotification(Event event) {
    if (event.enableNotifications && event.reminderOffset != null) {
      final notificationTime = event.targetDateTime.subtract(event.reminderOffset!);
      if (notificationTime.isAfter(DateTime.now())) {
        _notificationService.scheduleNotification(
          id: event.notificationId,
          title: "Reminder: ${event.name}",
          body: "Your event '${event.name}' is coming up in ${event.reminderOffset!.inMinutes} minutes!",
          scheduledDateTime: notificationTime,
        );
      }
    }
  }

  // Dispose of the timer when the provider is no longer needed
  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }
}

4. Notification Service

Set up flutter_local_notifications to handle scheduling and displaying notifications. You'll need to configure it for Android and iOS in their respective native project files.


// services/notification_service.dart
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest_all.dart' as tz;

class NotificationService {
  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

  Future<void> init() async {
    tz.initializeAll();
    final AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('@mipmap/ic_launcher');

    final DarwinInitializationSettings initializationSettingsIOS =
        DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: true,
    );

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

    await flutterLocalNotificationsPlugin.initialize(initializationSettings);
  }

  Future<void> scheduleNotification({
    required int id,
    required String title,
    required String body,
    required DateTime scheduledDateTime,
  }) async {
    await flutterLocalNotificationsPlugin.zonedSchedule(
      id,
      title,
      body,
      tz.TZDateTime.from(scheduledDateTime, tz.local),
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'countdown_channel',
          'Countdown Reminders',
          channelDescription: 'Reminders for your countdown events',
          importance: Importance.high,
          priority: Priority.high,
          ticker: 'ticker',
        ),
        iOS: DarwinNotificationDetails(),
      ),
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
      uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: DateTimeComponents.dayOfMonthAndTime,
    );
  }

  Future<void> cancelNotification(int id) async {
    await flutterLocalNotificationsPlugin.cancel(id);
  }
}

5. UI Implementation (Countdown Widget)

Create a widget to display the list of countdown events. Use Consumer from the provider package to react to changes in the CountdownProvider.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app_name/models/event.dart';
import 'package:your_app_name/providers/countdown_provider.dart'; // Adjust import path
import 'package:uuid/uuid.dart'; // For generating unique IDs

class CountdownScreen extends StatelessWidget {
  const CountdownScreen({super.key});

  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 Scaffold(
      appBar: AppBar(
        title: const Text('Multi-Event Countdown'),
      ),
      body: Consumer<CountdownProvider>(
        builder: (context, countdownProvider, child) {
          if (countdownProvider.events.isEmpty) {
            return const Center(
              child: Text('No events yet. Add one!'),
            );
          }
          return ListView.builder(
            itemCount: countdownProvider.events.length,
            itemBuilder: (context, index) {
              final event = countdownProvider.events[index];
              final duration = event.targetDateTime.difference(DateTime.now());

              return Card(
                margin: const EdgeInsets.all(8.0),
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        event.name,
                        style: const TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 8),
                      Text(
                        'Target: ${event.targetDateTime.toLocal().toString().split('.')[0]}',
                        style: const TextStyle(fontSize: 14, color: Colors.grey),
                      ),
                      const SizedBox(height: 8),
                      duration.isNegative
                          ? Text(
                              event.repeatInterval != RepeatInterval.none
                                  ? 'Event passed (will repeat)'
                                  : 'Event passed',
                              style: const TextStyle(
                                fontSize: 18,
                                color: Colors.red,
                                fontWeight: FontWeight.w600,
                              ),
                            )
                          : Text(
                              _formatDuration(duration),
                              style: const TextStyle(
                                fontSize: 24,
                                fontWeight: FontWeight.bold,
                                color: Colors.blueAccent,
                              ),
                            ),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.end,
                        children: [
                          IconButton(
                            icon: const Icon(Icons.edit),
                            onPressed: () {
                              // Navigate to an edit screen or show a dialog
                              _showAddEditEventDialog(context, event: event);
                            },
                          ),
                          IconButton(
                            icon: const Icon(Icons.delete),
                            onPressed: () {
                              countdownProvider.deleteEvent(event.id);
                            },
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _showAddEditEventDialog(context);
        },
        child: const Icon(Icons.add),
      ),
    );
  }

  void _showAddEditEventDialog(BuildContext context, {Event? event}) {
    final TextEditingController nameController = TextEditingController(text: event?.name ?? '');
    DateTime selectedDateTime = event?.targetDateTime ?? DateTime.now().add(const Duration(days: 1));
    Duration? reminderOffset = event?.reminderOffset;
    RepeatInterval selectedRepeatInterval = event?.repeatInterval ?? RepeatInterval.none;
    bool enableNotifications = event?.enableNotifications ?? true;

    showDialog(
      context: context,
      builder: (BuildContext dialogContext) {
        return StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return AlertDialog(
              title: Text(event == null ? 'Add New Event' : 'Edit Event'),
              content: SingleChildScrollView(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    TextField(
                      controller: nameController,
                      decoration: const InputDecoration(labelText: 'Event Name'),
                    ),
                    ListTile(
                      title: Text('Date: ${selectedDateTime.toLocal().toString().split(' ')[0]}'),
                      trailing: const Icon(Icons.calendar_today),
                      onTap: () async {
                        DateTime? picked = await showDatePicker(
                          context: context,
                          initialDate: selectedDateTime,
                          firstDate: DateTime.now(),
                          lastDate: DateTime(2101),
                        );
                        if (picked != null) {
                          setState(() {
                            selectedDateTime = DateTime(
                              picked.year, picked.month, picked.day,
                              selectedDateTime.hour, selectedDateTime.minute, selectedDateTime.second,
                            );
                          });
                        }
                      },
                    ),
                    ListTile(
                      title: Text('Time: ${selectedDateTime.toLocal().toString().split(' ')[1].substring(0, 5)}'),
                      trailing: const Icon(Icons.access_time),
                      onTap: () async {
                        TimeOfDay? picked = await showTimePicker(
                          context: context,
                          initialTime: TimeOfDay.fromDateTime(selectedDateTime),
                        );
                        if (picked != null) {
                          setState(() {
                            selectedDateTime = DateTime(
                              selectedDateTime.year, selectedDateTime.month, selectedDateTime.day,
                              picked.hour, picked.minute,
                            );
                          });
                        }
                      },
                    ),
                    DropdownButton<RepeatInterval>(
                      value: selectedRepeatInterval,
                      onChanged: (RepeatInterval? newValue) {
                        if (newValue != null) {
                          setState(() {
                            selectedRepeatInterval = newValue;
                          });
                        }
                      },
                      items: RepeatInterval.values
                          .map<DropdownMenuItem<RepeatInterval>>((RepeatInterval value) {
                        return DropdownMenuItem<RepeatInterval>(
                          value: value,
                          child: Text(value.toString().split('.').last),
                        );
                      }).toList(),
                    ),
                    CheckboxListTile(
                      title: const Text('Enable Notifications'),
                      value: enableNotifications,
                      onChanged: (bool? value) {
                        setState(() {
                          enableNotifications = value ?? false;
                        });
                      },
                    ),
                    if (enableNotifications)
                      DropdownButton<Duration>(
                        value: reminderOffset,
                        hint: const Text('Reminder before event'),
                        onChanged: (Duration? newValue) {
                          setState(() {
                            reminderOffset = newValue;
                          });
                        },
                        items: const [
                          DropdownMenuItem(value: Duration(minutes: 5), child: Text('5 minutes before')),
                          DropdownMenuItem(value: Duration(minutes: 15), child: Text('15 minutes before')),
                          DropdownMenuItem(value: Duration(hours: 1), child: Text('1 hour before')),
                          DropdownMenuItem(value: Duration(days: 1), child: Text('1 day before')),
                        ],
                      ),
                  ],
                ),
              ),
              actions: [
                TextButton(
                  onPressed: () => Navigator.pop(dialogContext),
                  child: const Text('Cancel'),
                ),
                ElevatedButton(
                  onPressed: () {
                    if (nameController.text.isNotEmpty) {
                      final newEvent = Event(
                        id: event?.id ?? const Uuid().v4(),
                        name: nameController.text,
                        targetDateTime: selectedDateTime,
                        reminderOffset: reminderOffset,
                        enableNotifications: enableNotifications,
                        repeatInterval: selectedRepeatInterval,
                        notificationId: event?.notificationId ?? DateTime.now().millisecondsSinceEpoch % 100000, // Simple unique ID
                      );
                      if (event == null) {
                        Provider.of<CountdownProvider>(context, listen: false).addEvent(newEvent);
                      } else {
                        Provider.of<CountdownProvider>(context, listen: false).updateEvent(newEvent);
                      }
                      Navigator.pop(dialogContext);
                    }
                  },
                  child: Text(event == null ? 'Add' : 'Save'),
                ),
              ],
            );
          },
        );
      },
    );
  }
}

6. Main Application Integration

Wrap your MaterialApp with a ChangeNotifierProvider to make the CountdownProvider available throughout your app.


// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app_name/providers/countdown_provider.dart';
import 'package:your_app_name/screens/countdown_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => CountdownProvider(),
      child: MaterialApp(
        title: 'Multi-Event Countdown',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const CountdownScreen(),
      ),
    );
  }
}

Challenges and Solutions

  • Background Execution: Local notifications handle background reminders effectively. For more complex background logic (e.g., updating UI when the app is killed), consider using workmanager or platform-specific background services, which is beyond the scope of this basic setup.
  • Time Zone Handling: Always store DateTime objects in UTC and convert to local time for display or notification scheduling. The timezone package is crucial for accurate local time-based notifications.
  • Persistent Storage: For a real-world application, events should be saved to persistent storage (e.g., SQLite with sqflite, Hive, or shared preferences) so they are not lost when the app is closed. Load events from storage in the CountdownProvider constructor.
  • Unique Notification IDs: Ensure each scheduled notification has a unique ID to prevent overwriting. Using DateTime.now().millisecondsSinceEpoch or a UUID generator for event IDs and derived notification IDs is a good strategy.

Conclusion

Building a multi-event countdown timer with advanced features like reminders, notifications, repeat functionality, and custom labels in Flutter is a powerful way to enhance user engagement. By leveraging Flutter's state management (Provider), local notification plugins, and a well-defined data model, developers can create a highly functional and intuitive application that caters to diverse event management needs. This foundation provides a strong starting point for further enhancements, such as integration with calendars, cloud synchronization, or more elaborate UI designs.

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