image

15 Mar 2026

9K

35K

Building a Multi-Event Countdown Timer Widget with Reminders and Notifications in Flutter

Countdown timers are a popular UI element, useful for tracking upcoming events, deadlines, or special occasions. A multi-event countdown timer takes this concept further, allowing users to monitor several events simultaneously, enhanced with intelligent reminders and notifications to ensure no important date is missed. This article delves into creating such a robust widget in Flutter, covering event data modeling, real-time countdown logic, and effective local notification scheduling.

1. Project Setup and Dependencies

First, create a new Flutter project and add the necessary dependencies to your pubspec.yaml file. We'll primarily need flutter_local_notifications for handling notifications.


dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  flutter_local_notifications: ^17.1.2 # Use the latest stable version
  intl: ^0.19.0 # For date formatting, if needed

After adding, run flutter pub get.

2. Initializing Local Notifications

Before scheduling any notifications, flutter_local_notifications must be initialized. This typically happens in your main.dart file.


import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future main() async {
  WidgetsFlutterBinding.ensureInitialized();

  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 notificationResponse) async {
      // Handle notification tap here if needed
      debugPrint('Notification tapped: ${notificationResponse.payload}');
    },
  );

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Multi-Event Countdown',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MultiEventCountdownScreen(),
    );
  }
}

3. Defining the Event Model

An Event class will hold the details for each countdown item. It should include a unique ID, name, target date and time, and potentially a notification preference.


class Event {
  final int id;
  final String name;
  final DateTime targetDateTime;
  final Duration? reminderOffset; // How long before the event to remind

  Event({
    required this.id,
    required this.name,
    required this.targetDateTime,
    this.reminderOffset,
  });
}

4. Implementing the Countdown Logic

The core of our widget is the countdown logic. We'll need a StatefulWidget that updates periodically to reflect the remaining time.


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

class EventCountdownWidget extends StatefulWidget {
  final Event event;

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

  @override
  State createState() => _EventCountdownWidgetState();
}

class _EventCountdownWidgetState extends State {
  late Duration _timeRemaining;
  Timer? _timer;

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

  void _startTimer() {
    _timeRemaining = widget.event.targetDateTime.difference(DateTime.now());
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (mounted) {
        setState(() {
          _timeRemaining = widget.event.targetDateTime.difference(DateTime.now());
          if (_timeRemaining.isNegative) {
            _timeRemaining = Duration.zero;
            _timer?.cancel();
          }
        });
      }
    });
  }

  String _formatDuration(Duration duration) {
    if (duration == Duration.zero) return 'Event has passed!';

    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 d $hours h $minutes m $seconds s';
  }

  @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.name,
              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text(
              'Target: ${DateFormat.yMMMd().add_Hms().format(widget.event.targetDateTime)}',
              style: const TextStyle(fontSize: 14, color: Colors.grey),
            ),
            const SizedBox(height: 16),
            Text(
              _formatDuration(_timeRemaining),
              style: TextStyle(
                fontSize: _timeRemaining == Duration.zero ? 18 : 24,
                color: _timeRemaining == Duration.zero ? Colors.redAccent : Colors.blue,
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
      ),
    );
  }

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

5. Scheduling Notifications and Reminders

This is where flutter_local_notifications shines. We can schedule one-time notifications for the exact event time and also for custom reminder times before the event.


import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'event_model.dart'; // Make sure this path is correct

class NotificationService {
  final FlutterLocalNotificationsPlugin notificationsPlugin;

  NotificationService({required this.notificationsPlugin}) {
    tz.initializeTimeZones();
  }

  Future scheduleEventNotification(Event event) async {
    final scheduledTime = tz.TZDateTime.from(
      event.targetDateTime,
      tz.local,
    );

    if (scheduledTime.isBefore(tz.TZDateTime.now(tz.local))) {
      // Don't schedule notifications for past events
      return;
    }

    // Schedule notification for the event itself
    await notificationsPlugin.zonedSchedule(
      event.id, // Unique ID for this notification
      'Event Reminder: ${event.name}',
      'It\'s time for "${event.name}"!',
      scheduledTime,
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'event_channel_id',
          'Event Notifications',
          channelDescription: 'Notifications for upcoming events',
          importance: Importance.max,
          priority: Priority.high,
          ticker: 'ticker',
        ),
        iOS: DarwinNotificationDetails(),
      ),
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
      uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: DateTimeComponents.dayOfMonthAndTime,
      payload: 'event_id_${event.id}',
    );

    // Schedule reminder notification if offset is provided
    if (event.reminderOffset != null) {
      final reminderTime = scheduledTime.subtract(event.reminderOffset!);
      if (reminderTime.isAfter(tz.TZDateTime.now(tz.local))) {
        await notificationsPlugin.zonedSchedule(
          event.id + 1000, // Use a different ID for the reminder
          'Upcoming: ${event.name}',
          '${event.name} is starting in ${event.reminderOffset!.inMinutes} minutes!',
          reminderTime,
          const NotificationDetails(
            android: AndroidNotificationDetails(
              'reminder_channel_id',
              'Event Reminders',
              channelDescription: 'Reminders before events',
              importance: Importance.defaultImportance,
              priority: Priority.defaultPriority,
            ),
            iOS: DarwinNotificationDetails(),
          ),
          androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
          uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
          matchDateTimeComponents: DateTimeComponents.dayOfMonthAndTime,
          payload: 'reminder_event_id_${event.id}',
        );
      }
    }
  }

  Future cancelEventNotifications(int eventId) async {
    await notificationsPlugin.cancel(eventId); // Cancel event notification
    await notificationsPlugin.cancel(eventId + 1000); // Cancel reminder notification
  }
}

Remember to add timezone to your pubspec.yaml:


dependencies:
  ...
  timezone: ^0.9.0

6. The Multi-Event Countdown Screen

Finally, we'll create a screen to display a list of events and manage their notifications.


import 'package:flutter/material.dart';
import 'event_model.dart';
import 'countdown_widget.dart';
import 'notification_service.dart'; // Adjust path as needed

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

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

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

  @override
  void initState() {
    super.initState();
    _notificationService = NotificationService(notificationsPlugin: flutterLocalNotificationsPlugin);
    _loadSampleEvents();
  }

  void _loadSampleEvents() {
    // Add some sample events
    _addEvent(Event(
      id: 1,
      name: 'Project Deadline',
      targetDateTime: DateTime.now().add(const Duration(days: 3, hours: 10, minutes: 30)),
      reminderOffset: const Duration(hours: 1), // Remind 1 hour before
    ));
    _addEvent(Event(
      id: 2,
      name: 'Birthday Party',
      targetDateTime: DateTime.now().add(const Duration(days: 7, hours: 18)),
      reminderOffset: const Duration(days: 1), // Remind 1 day before
    ));
    _addEvent(Event(
      id: 3,
      name: 'Vacation Start',
      targetDateTime: DateTime.now().add(const Duration(days: 30)),
      reminderOffset: const Duration(days: 7), // Remind 1 week before
    ));
  }

  void _addEvent(Event event) {
    setState(() {
      _events.add(event);
    });
    _notificationService.scheduleEventNotification(event);
  }

  void _removeEvent(Event event) {
    setState(() {
      _events.removeWhere((e) => e.id == event.id);
    });
    _notificationService.cancelEventNotifications(event.id);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Multi-Event Countdown'),
      ),
      body: _events.isEmpty
          ? const Center(
              child: Text(
                'No events scheduled. Add a new event!',
                style: TextStyle(fontSize: 18, color: Colors.grey),
              ),
            )
          : ListView.builder(
              itemCount: _events.length,
              itemBuilder: (context, index) {
                final event = _events[index];
                return Dismissible(
                  key: ValueKey(event.id),
                  direction: DismissDirection.endToStart,
                  onDismissed: (direction) {
                    _removeEvent(event);
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('${event.name} dismissed')),
                    );
                  },
                  background: Container(
                    color: Colors.red,
                    alignment: Alignment.centerRight,
                    padding: const EdgeInsets.symmetric(horizontal: 20),
                    child: const Icon(Icons.delete, color: Colors.white),
                  ),
                  child: EventCountdownWidget(event: event),
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Implement a dialog or new screen to add events dynamically
          // For now, let's just add a placeholder event
          final newId = _events.isEmpty ? 1 : _events.last.id + 1;
          final newEvent = Event(
            id: newId,
            name: 'New Custom Event $newId',
            targetDateTime: DateTime.now().add(const Duration(minutes: 5) * newId),
            reminderOffset: const Duration(minutes: 1),
          );
          _addEvent(newEvent);
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

7. Refinements and Considerations

  • State Management: For more complex applications, consider using state management solutions like Provider, Riverpod, or BLoC to manage the list of events and their updates more efficiently.
  • Persistent Storage: Events should ideally be stored persistently using solutions like shared_preferences, SQLite (via sqflite), or a cloud database. When the app restarts, scheduled notifications for future events need to be re-registered after fetching them from storage.
  • Notification Customization: Explore more advanced features of flutter_local_notifications, such as custom sounds, vibration patterns, large icon support, and action buttons.
  • User Experience: Provide clear UI for adding, editing, and deleting events. Consider allowing users to customize reminder times.
  • Background Execution: While flutter_local_notifications handles background scheduling, if your app needs to perform complex logic or fetch data in the background to update notifications, you might need workmanager or platform-specific background tasks.
  • Time Zone Handling: The timezone package is crucial for correctly scheduling notifications irrespective of the user's current time zone. Ensure proper initialization and usage.

Conclusion

Building a multi-event countdown timer with reminders and notifications in Flutter involves careful integration of UI updates, event data management, and platform-specific notification services. By following the steps outlined, you can create a highly functional and engaging widget that keeps users informed about their important dates and deadlines. Further enhancements can include rich UI animations, category-based event filtering, and synchronization with calendar services for a truly comprehensive experience.

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