image

25 Apr 2026

9K

35K

Creating a Multi-Event Countdown Timer Widget with Notification and Repeat Feature in Flutter

Building a robust multi-event countdown timer in Flutter involves managing multiple distinct timers, handling their completion, scheduling local notifications, and implementing flexible repeat functionalities. This article guides you through creating such a widget, leveraging Flutter's reactive UI capabilities and integrating with a local notification package.

Core Concepts and Dependencies

At the heart of our solution will be a data model for each event, a state management approach to update the UI efficiently, a timer mechanism to track time, and a notification service. We'll use the following key dependencies:

  • flutter_local_notifications: For scheduling and displaying local notifications.
  • provider: A widely used package for state management, making it easier to share event data across widgets.
  • intl: For convenient date and time formatting.

First, add these dependencies to your pubspec.yaml file:


dependencies:
  flutter:
    sdk: flutter
  flutter_local_notifications: ^17.1.2
  provider: ^6.1.2
  intl: ^0.19.0

After adding, run flutter pub get.

1. The Event Data Model

We need a class to represent each countdown event, including its target time, a unique ID, notification status, and repetition details.


// lib/models/countdown_event.dart
import 'package:flutter/foundation.dart';

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

class CountdownEvent {
  final String id;
  String title;
  DateTime targetDateTime;
  bool enableNotifications;
  RepeatInterval repeat;

  CountdownEvent({
    required this.id,
    required this.title,
    required this.targetDateTime,
    this.enableNotifications = true,
    this.repeat = RepeatInterval.none,
  });

  Duration get remainingDuration => targetDateTime.difference(DateTime.now());

  void advanceTargetDateTime() {
    DateTime now = DateTime.now();
    switch (repeat) {
      case RepeatInterval.daily:
        while (targetDateTime.isBefore(now)) {
          targetDateTime = targetDateTime.add(const Duration(days: 1));
        }
        break;
      case RepeatInterval.weekly:
        while (targetDateTime.isBefore(now)) {
          targetDateTime = targetDateTime.add(const Duration(days: 7));
        }
        break;
      case RepeatInterval.monthly:
      // Note: Handling months precisely is complex due to varying days.
      // A simple approach is to add a month, then adjust day if target day
      // is greater than days in new month. For simplicity, we add 30 days.
        while (targetDateTime.isBefore(now)) {
          targetDateTime = DateTime(
            targetDateTime.year,
            targetDateTime.month + 1,
            targetDateTime.day,
            targetDateTime.hour,
            targetDateTime.minute,
            targetDateTime.second,
          );
        }
        break;
      case RepeatInterval.yearly:
        while (targetDateTime.isBefore(now)) {
          targetDateTime = DateTime(
            targetDateTime.year + 1,
            targetDateTime.month,
            targetDateTime.day,
            targetDateTime.hour,
            targetDateTime.minute,
            targetDateTime.second,
          );
        }
        break;
      case RepeatInterval.none:
        // Do nothing for non-repeating events
        break;
    }
  }
}

2. Notification Service

This service handles initializing flutter_local_notifications and scheduling notifications for our events.


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

class NotificationService {
  static final NotificationService _notificationService = NotificationService._internal();

  factory NotificationService() {
    return _notificationService;
  }

  NotificationService._internal();

  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();

  Future init() async {
    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,
            macOS: initializationSettingsIOS); // Also use iOS settings for macOS

    tz.initializeTimeZones();
    // Set the local location to your device's timezone
    tz.setLocalLocation(tz.getLocation('Asia/Jakarta')); // Example: set your desired timezone

    await flutterLocalNotificationsPlugin.initialize(initializationSettings);
  }

  Future scheduleNotification({
    required int id,
    required String title,
    required String body,
    required DateTime scheduledDate,
  }) async {
    if (scheduledDate.isBefore(DateTime.now())) {
      return; // Do not schedule past notifications
    }

    await flutterLocalNotificationsPlugin.zonedSchedule(
      id,
      title,
      body,
      tz.TZDateTime.from(scheduledDate, tz.local),
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'countdown_channel',
          'Countdown Notifications',
          channelDescription: 'Notifications for your countdown events',
          importance: Importance.max,
          priority: Priority.high,
          ticker: 'ticker',
        ),
        iOS: DarwinNotificationDetails(),
      ),
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
      uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: null, // Schedule exact time
    );
  }

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

3. Countdown Manager

This ChangeNotifier will manage our list of CountdownEvent objects. It will update the events' remaining durations, handle completion, trigger repetitions, and integrate with the notification service.


// lib/providers/countdown_manager.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:multi_event_countdown/models/countdown_event.dart';
import 'package:multi_event_countdown/services/notification_service.dart';

class CountdownManager with ChangeNotifier {
  final List _events = [];
  Timer? _timer;
  final NotificationService _notificationService = NotificationService();

  List get events => _events;

  CountdownManager() {
    _startTimer();
  }

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

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

  void removeEvent(String id) {
    _events.removeWhere((event) {
      if (event.id == id) {
        _notificationService.cancelNotification(int.parse(event.id));
        return true;
      }
      return false;
    });
    notifyListeners();
  }

  void _startTimer() {
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      _checkEvents();
      notifyListeners(); // Notify listeners every second to update UI
    });
  }

  void _checkEvents() {
    List completedEvents = [];
    for (var event in _events) {
      if (event.remainingDuration.isNegative) {
        completedEvents.add(event);
      }
    }

    for (var event in completedEvents) {
      if (event.repeat != RepeatInterval.none) {
        _handleRepeatingEvent(event);
      } else {
        _handleCompletedEvent(event);
      }
    }
  }

  void _handleCompletedEvent(CountdownEvent event) {
    _notificationService.cancelNotification(int.parse(event.id));
    // Optionally remove the event if it's not repeating.
    // For this example, we'll keep it as a "past event" until manually removed.
    // _events.remove(event);
  }

  void _handleRepeatingEvent(CountdownEvent event) {
    _notificationService.cancelNotification(int.parse(event.id)); // Cancel existing
    event.advanceTargetDateTime();
    _scheduleEventNotification(event); // Schedule new
  }

  void _scheduleEventNotification(CountdownEvent event) {
    if (event.enableNotifications && event.remainingDuration.isPositive) {
      _notificationService.scheduleNotification(
        id: int.parse(event.id),
        title: '${event.title} is happening now!',
        body: 'Your event "${event.title}" has started or is about to start.',
        scheduledDate: event.targetDateTime,
      );
    }
  }

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

4. The Countdown Widget

This widget will display a list of our countdown events. We'll create a single card widget for each event and a parent widget to manage the list.


// lib/widgets/countdown_card.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:multi_event_countdown/models/countdown_event.dart';

class CountdownCard extends StatelessWidget {
  final CountdownEvent event;
  final VoidCallback onEdit;
  final VoidCallback onDelete;

  const CountdownCard({
    Key? key,
    required this.event,
    required this.onEdit,
    required this.onDelete,
  }) : super(key: key);

  String _formatDuration(Duration duration) {
    if (duration.isNegative) {
      return "Event finished!";
    }
    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.symmetric(vertical: 8.0, horizontal: 16.0),
      elevation: 4.0,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                  child: Text(
                    event.title,
                    style: Theme.of(context).textTheme.headlineSmall,
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
                Row(
                  children: [
                    IconButton(
                      icon: const Icon(Icons.edit),
                      onPressed: onEdit,
                    ),
                    IconButton(
                      icon: const Icon(Icons.delete),
                      onPressed: onDelete,
                    ),
                  ],
                ),
              ],
            ),
            const SizedBox(height: 8.0),
            Text(
              'Target: ${DateFormat('yyyy-MM-dd HH:mm:ss').format(event.targetDateTime)}',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 4.0),
            Text(
              'Repeat: ${event.repeat.name.toUpperCase()}',
              style: Theme.of(context).textTheme.bodySmall,
            ),
            const SizedBox(height: 12.0),
            Center(
              child: Text(
                _formatDuration(event.remainingDuration),
                style: Theme.of(context).textTheme.headlineLarge?.copyWith(
                      color: event.remainingDuration.isNegative ? Colors.red : Colors.green,
                      fontWeight: FontWeight.bold,
                    ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart'; // Add uuid: ^4.3.3 to pubspec.yaml for unique IDs
import 'package:multi_event_countdown/models/countdown_event.dart';
import 'package:multi_event_countdown/providers/countdown_manager.dart';
import 'package:multi_event_countdown/widgets/countdown_card.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  void _showAddEditEventDialog(BuildContext context, {CountdownEvent? event}) {
    final TextEditingController titleController = TextEditingController(text: event?.title);
    DateTime selectedDateTime = event?.targetDateTime ?? DateTime.now();
    bool enableNotifications = event?.enableNotifications ?? true;
    RepeatInterval selectedRepeat = event?.repeat ?? RepeatInterval.none;

    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (ctx) {
        return StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return Padding(
              padding: EdgeInsets.only(
                bottom: MediaQuery.of(context).viewInsets.bottom,
                left: 16,
                right: 16,
                top: 16,
              ),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    event == null ? 'Add New Event' : 'Edit Event',
                    style: Theme.of(context).textTheme.headlineSmall,
                  ),
                  TextField(
                    controller: titleController,
                    decoration: const InputDecoration(labelText: 'Event Title'),
                  ),
                  ListTile(
                    title: Text(
                      'Target Date & Time: ${DateFormat('yyyy-MM-dd HH:mm').format(selectedDateTime)}',
                    ),
                    trailing: const Icon(Icons.calendar_today),
                    onTap: () async {
                      final DateTime? pickedDate = await showDatePicker(
                        context: context,
                        initialDate: selectedDateTime,
                        firstDate: DateTime.now().subtract(const Duration(days: 365)),
                        lastDate: DateTime.now().add(const Duration(days: 365 * 10)),
                      );
                      if (pickedDate != null) {
                        final TimeOfDay? pickedTime = await showTimePicker(
                          context: context,
                          initialTime: TimeOfDay.fromDateTime(selectedDateTime),
                        );
                        if (pickedTime != null) {
                          setState(() {
                            selectedDateTime = DateTime(
                              pickedDate.year,
                              pickedDate.month,
                              pickedDate.day,
                              pickedTime.hour,
                              pickedTime.minute,
                            );
                          });
                        }
                      }
                    },
                  ),
                  Row(
                    children: [
                      const Text('Enable Notifications:'),
                      Switch(
                        value: enableNotifications,
                        onChanged: (value) {
                          setState(() {
                            enableNotifications = value;
                          });
                        },
                      ),
                    ],
                  ),
                  DropdownButton(
                    value: selectedRepeat,
                    onChanged: (RepeatInterval? newValue) {
                      if (newValue != null) {
                        setState(() {
                          selectedRepeat = newValue;
                        });
                      }
                    },
                    items: RepeatInterval.values.map>(
                      (RepeatInterval value) {
                        return DropdownMenuItem(
                          value: value,
                          child: Text(value.name.toUpperCase()),
                        );
                      },
                    ).toList(),
                  ),
                  const SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () {
                      if (titleController.text.isEmpty) return;
                      final manager = Provider.of(context, listen: false);
                      if (event == null) {
                        manager.addEvent(CountdownEvent(
                          id: const Uuid().v4(),
                          title: titleController.text,
                          targetDateTime: selectedDateTime,
                          enableNotifications: enableNotifications,
                          repeat: selectedRepeat,
                        ));
                      } else {
                        manager.updateEvent(CountdownEvent(
                          id: event.id,
                          title: titleController.text,
                          targetDateTime: selectedDateTime,
                          enableNotifications: enableNotifications,
                          repeat: selectedRepeat,
                        ));
                      }
                      Navigator.pop(context);
                    },
                    child: Text(event == null ? 'Add Event' : 'Save Changes'),
                  ),
                  const SizedBox(height: 10),
                ],
              ),
            );
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Multi-Event Countdown Timer'),
      ),
      body: Consumer(
        builder: (context, manager, child) {
          if (manager.events.isEmpty) {
            return const Center(
              child: Text('No countdown events yet! Tap + to add one.'),
            );
          }
          return ListView.builder(
            itemCount: manager.events.length,
            itemBuilder: (context, index) {
              final event = manager.events[index];
              return CountdownCard(
                event: event,
                onEdit: () => _showAddEditEventDialog(context, event: event),
                onDelete: () => manager.removeEvent(event.id),
              );
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddEditEventDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }
}

5. Putting It All Together (main.dart)

Finally, we initialize our services and wrap our app with the ChangeNotifierProvider to make our CountdownManager accessible throughout the widget tree.


// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:multi_event_countdown/providers/countdown_manager.dart';
import 'package:multi_event_countdown/screens/home_screen.dart';
import 'package:multi_event_countdown/services/notification_service.dart';

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 ChangeNotifierProvider(
      create: (context) => CountdownManager(),
      child: MaterialApp(
        title: 'Multi-Event Countdown',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: const HomeScreen(),
      ),
    );
  }
}

Advanced Considerations

  • Persistence: Currently, events are lost when the app restarts. You can implement persistence using packages like shared_preferences or a database like SQLite (with sqflite) to save and load events.
  • Background Execution: For highly accurate background notifications and timer updates, consider using Flutter's background services (e.g., workmanager) or native platform-specific background tasks. This is crucial if you need timers to function even when the app is terminated.
  • User Interface: Enhance the UI with more visual feedback, custom animations, and better input forms for adding/editing events.
  • Error Handling: Add more robust error handling for notification scheduling and date parsing.

Conclusion

You now have a foundational multi-event countdown timer widget in Flutter, complete with local notifications and a repeat feature. This setup provides a solid base that can be expanded with persistence, more complex repeat logic, and a refined user experience to meet the specific needs of your application.

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