image

18 Mar 2026

9K

35K

Building an Event Card Widget with Countdown Timer in Flutter

Introduction

In modern applications, displaying upcoming events with a real-time countdown timer is a common and highly engaging feature. Whether it's for a product launch, a webinar, or a conference, a visual countdown builds anticipation and helps users keep track of important dates. This article will guide you through building a dynamic Event Card widget in Flutter, complete with a live countdown timer.

Prerequisites

To follow along with this tutorial, you should have:

  • Basic knowledge of Flutter and Dart.
  • Flutter SDK installed and configured.
  • An IDE like VS Code or Android Studio.

Core Concepts

Before diving into the code, let's briefly touch upon the core Flutter and Dart concepts we'll be utilizing:

  • StatefulWidget: Our Event Card will need to update its UI dynamically every second as the countdown changes, making StatefulWidget the perfect choice.
  • DateTime: Dart's DateTime class will be used to represent the event's target time and the current time.
  • Duration: The difference between two DateTime objects results in a Duration, which can then be broken down into days, hours, minutes, and seconds.
  • Timer.periodic: This Dart class allows us to execute a callback function repeatedly at specified intervals, which is essential for updating the countdown every second.
  • setState(): Called within our StatefulWidget, this method will trigger a rebuild of the widget's UI, reflecting the updated countdown time.

Step-by-Step Implementation

1. Setting Up the Project

First, create a new Flutter project if you haven't already:


flutter create event_countdown_app
cd event_countdown_app

Then, open the project in your preferred IDE.

2. Designing the Basic Event Card UI

We'll start by creating the foundational structure for our event card. This will be a StatefulWidget that takes the event's title, location, and target DateTime as parameters.

Create a new file, say lib/event_card.dart, and add the following code:


import 'package:flutter/material.dart';

class EventCard extends StatefulWidget {
  final String eventTitle;
  final String eventLocation;
  final DateTime eventDateTime;

  const EventCard({
    Key? key,
    required this.eventTitle,
    required this.eventLocation,
    required this.eventDateTime,
  }) : super(key: key);

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

class _EventCardState extends State {
  // Placeholder for countdown text
  String _countdownText = 'Loading...';

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(16.0),
      elevation: 4,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              widget.eventTitle,
              style: Theme.of(context).textTheme.headline5?.copyWith(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text(
              widget.eventLocation,
              style: Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.grey[700]),
            ),
            const SizedBox(height: 16),
            const Divider(),
            const SizedBox(height: 16),
            Row(
              children: [
                const Icon(Icons.timer, color: Colors.blue),
                const SizedBox(width: 8),
                Text(
                  _countdownText, // This will be updated by the timer
                  style: Theme.of(context).textTheme.headline6?.copyWith(color: Colors.blue),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

3. Implementing the Countdown Logic

Now, let's add the heart of our widget: the countdown timer. We'll use dart:async for the Timer class.

Inside _EventCardState, add the necessary imports, state variables, and methods:


import 'dart:async'; // Don't forget this import!
import 'package:flutter/material.dart';

// ... (Rest of EventCard class definition)

class _EventCardState extends State {
  Duration _timeRemaining = Duration.zero;
  Timer? _timer;
  String _countdownText = ''; // Initialize empty or with a default

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

  @override
  void dispose() {
    _timer?.cancel(); // Important: Cancel the timer to prevent memory leaks
    super.dispose();
  }

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

    // If event is in the past, set duration to zero and update text
    if (_timeRemaining.isNegative) {
      _timeRemaining = Duration.zero;
      _updateCountdownText();
      return;
    }

    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        _timeRemaining = widget.eventDateTime.difference(DateTime.now());
        _updateCountdownText();
        if (_timeRemaining.isNegative) {
          _timeRemaining = Duration.zero; // Ensure it doesn't go negative on display
          _timer?.cancel(); // Stop timer when event starts/passes
        }
      });
    });
  }

  void _updateCountdownText() {
    if (_timeRemaining == Duration.zero) {
      _countdownText = 'Event Has Started!';
    } else if (_timeRemaining.isNegative) { // Should ideally be caught earlier, but good for safety
      _countdownText = 'Event Has Passed!';
    } else {
      int days = _timeRemaining.inDays;
      int hours = _timeRemaining.inHours % 24;
      int minutes = _timeRemaining.inMinutes % 60;
      int seconds = _timeRemaining.inSeconds % 60;

      String formattedHours = hours.toString().padLeft(2, '0');
      String formattedMinutes = minutes.toString().padLeft(2, '0');
      String formattedSeconds = seconds.toString().padLeft(2, '0');

      if (days > 0) {
        _countdownText = '${days}d $formattedHours:$formattedMinutes:$formattedSeconds';
      } else {
        _countdownText = '$formattedHours:$formattedMinutes:$formattedSeconds';
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    // ... (rest of the build method from step 2)
    // Ensure _countdownText is used in the Text widget for the countdown
    return Card(
      margin: const EdgeInsets.all(16.0),
      elevation: 4,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              widget.eventTitle,
              style: Theme.of(context).textTheme.headline5?.copyWith(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text(
              widget.eventLocation,
              style: Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.grey[700]),
            ),
            const SizedBox(height: 16),
            const Divider(),
            const SizedBox(height: 16),
            Row(
              children: [
                const Icon(Icons.timer, color: Colors.blue),
                const SizedBox(width: 8),
                Text(
                  _countdownText, // This will now display the live countdown
                  style: Theme.of(context).textTheme.headline6?.copyWith(color: Colors.blue),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Explanation:

  • We introduce _timeRemaining (a Duration) to store the difference between the event time and now.
  • _timer (a Timer) is declared to hold our periodic timer instance.
  • initState() calls _startTimer() when the widget is created.
  • dispose() is crucial: it cancels the timer to prevent resource leaks when the widget is removed from the widget tree.
  • _startTimer() initializes _timeRemaining and then sets up a Timer.periodic that fires every second.
  • Inside the timer's callback, setState() is called to recalculate _timeRemaining, update _countdownText, and rebuild the UI.
  • If _timeRemaining becomes zero or negative, the timer is canceled, and a "Event Has Started!" message is displayed.
  • _updateCountdownText() formats the Duration into a user-friendly string. It uses padLeft(2, '0') to ensure single-digit numbers are padded with a leading zero (e.g., 5 becomes 05).

4. Integrating into Your App

Finally, let's use our EventCard widget in the main application file (lib/main.dart).


import 'package:flutter/material.dart';
import 'package:event_countdown_app/event_card.dart'; // Adjust import path if needed

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Event Countdown',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Upcoming Events'),
          centerTitle: true,
        ),
        body: ListView(
          padding: const EdgeInsets.symmetric(vertical: 16.0),
          children: [
            EventCard(
              eventTitle: 'Flutter Global Summit',
              eventLocation: 'Online via YouTube Live',
              eventDateTime: DateTime.now().add(const Duration(days: 7, hours: 10, minutes: 45, seconds: 30)),
            ),
            EventCard(
              eventTitle: 'Dart Language Update Webinar',
              eventLocation: 'Zoom Meeting',
              eventDateTime: DateTime.now().add(const Duration(hours: 2, minutes: 15)),
            ),
            EventCard(
              eventTitle: 'Local Dev Meetup',
              eventLocation: 'Community Hall A',
              eventDateTime: DateTime.now().add(const Duration(minutes: 5)),
            ),
            EventCard(
              eventTitle: 'Past Event Example',
              eventLocation: 'Virtual',
              eventDateTime: DateTime.now().subtract(const Duration(hours: 3, minutes: 20)), // An event in the past
            ),
          ],
        ),
      ),
    );
  }
}

Run your application:


flutter run

You should now see a list of event cards, each displaying a live countdown timer that updates every second!

Conclusion

You've successfully built a reusable Event Card widget with a real-time countdown timer in Flutter. This widget effectively demonstrates the power of StatefulWidget for dynamic UI updates, the utility of Dart's DateTime and Duration classes for time calculations, and the importance of resource management with Timer.periodic and dispose(). You can further enhance this widget by adding more styling, animations, or even different display formats for the countdown based on the remaining time.

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