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

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

Aug 02, 2026

Building a Flutter Notification Center with Grouping & Actions

Learn to create a sophisticated in-app notification center in Flutter, featuring grouping, swipe-to-dismiss, custom icons, and actionable buttons for enhanced u

Aug 01, 2026

Flutter Layout Tips: Using FractionallySizedBox, AspectRatio, ConstrainedBox for Adaptive UI

Master Flutter's FractionallySizedBox, AspectRatio, and ConstrainedBox to build truly adaptive UIs. Learn how to control widget sizing, maintain proportions, an

Aug 01, 2026

Flutter Product Quick View: Animation, Related Items, Badges

Learn to create a dynamic product quick view widget in Flutter, incorporating smooth animations, relevant related items, and eye-catching promo badges for an en