image

19 Dec 2025

9K

35K

Building a Countdown Timer Widget in Flutter

Countdown timers are a fundamental component in many modern applications, ranging from e-commerce platforms indicating sales deadlines, event apps counting down to a live stream, to productivity tools enforcing work intervals. Building a robust and reusable countdown timer in Flutter requires careful consideration of state management, asynchronous operations, and UI updates. This article will guide you through creating a professional and customizable countdown timer widget in Flutter.

Core Concepts for Countdown Timers

State Management with StatefulWidget

A countdown timer inherently involves dynamic changes to its displayed value over time. In Flutter, this dynamic nature necessitates the use of a StatefulWidget. A StatefulWidget allows you to manage mutable state that can change during the lifetime of the widget, and its associated State object provides methods like setState() to trigger UI rebuilds when the state changes.

The Timer Class from dart:async

To implement the periodic decrement of the countdown, Flutter leverages the Timer class available in the dart:async library. Specifically, Timer.periodic() is ideal for this purpose as it creates a repeating timer that invokes a callback function at regular intervals, allowing us to update the remaining time.

Lifecycle Management: initState and dispose

Managing the lifecycle of a timer is crucial for performance and preventing memory leaks. The initState method of a StatefulWidget is where we typically initialize the timer when the widget is first created. Conversely, the dispose method is essential for canceling the timer when the widget is removed from the widget tree, ensuring that background operations cease and resources are properly released.

Implementing the Countdown Timer Widget

Hereโ€™s the complete code for a customizable CountdownTimerWidget. This widget will take a Duration as an argument, representing the total time for the countdown.


import 'package:flutter/material.dart';
import 'dart:async'; // Required for the Timer class

class CountdownTimerWidget extends StatefulWidget {
  final Duration duration;
  final VoidCallback? onTimerEnd;
  final TextStyle? textStyle;
  final Color? backgroundColor;
  final EdgeInsetsGeometry? padding;

  const CountdownTimerWidget({
    Key? key,
    required this.duration,
    this.onTimerEnd,
    this.textStyle,
    this.backgroundColor,
    this.padding,
  }) : super(key: key);

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

class _CountdownTimerWidgetState extends State {
  late Timer _timer;
  late Duration _remainingDuration;

  @override
  void initState() {
    super.initState();
    _remainingDuration = widget.duration;
    _startTimer();
  }

  void _startTimer() {
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (_remainingDuration.inSeconds <= 0) {
        timer.cancel();
        if (widget.onTimerEnd != null) {
          widget.onTimerEnd!();
        }
      } else {
        setState(() {
          _remainingDuration = _remainingDuration - const Duration(seconds: 1);
        });
      }
    });
  }

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

  String _formatDuration(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, "0");
    String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
    String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
    if (duration.inHours > 0) {
      return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
    }
    return "$twoDigitMinutes:$twoDigitSeconds";
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: widget.padding ?? const EdgeInsets.all(8.0),
      decoration: BoxDecoration(
        color: widget.backgroundColor ?? Colors.blueAccent,
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Text(
        _formatDuration(_remainingDuration),
        style: widget.textStyle ??
            const TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
              color: Colors.white,
            ),
      ),
    );
  }
}

Code Breakdown and Explanation

1. Widget Definition and State Variables

The CountdownTimerWidget is a StatefulWidget that accepts a Duration, an optional onTimerEnd callback, and styling parameters. Its state class, _CountdownTimerWidgetState, holds a _timer instance of type Timer and a _remainingDuration variable to track the current time.

2. Initializing the Timer (initState)

In initState, we initialize _remainingDuration with the duration passed to the widget. Then, we call _startTimer() to begin the countdown process.


  @override
  void initState() {
    super.initState();
    _remainingDuration = widget.duration;
    _startTimer();
  }

3. The Timer Callback (_startTimer)

The _startTimer method sets up a Timer.periodic that fires every second. Inside the callback function:

  • It checks if _remainingDuration has reached zero or less. If so, the timer is cancelled using timer.cancel(), and the onTimerEnd callback (if provided) is invoked.
  • Otherwise, it decrements _remainingDuration by one second and calls setState(). This crucial step notifies Flutter that the state has changed and triggers a rebuild of the widget's UI to display the updated time.

  void _startTimer() {
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (_remainingDuration.inSeconds <= 0) {
        timer.cancel();
        if (widget.onTimerEnd != null) {
          widget.onTimerEnd!();
        }
      } else {
        setState(() {
          _remainingDuration = _remainingDuration - const Duration(seconds: 1);
        });
      }
    });
  }

4. Cleaning Up (dispose)

The dispose method is overridden to ensure that _timer.cancel() is called. This prevents memory leaks and unnecessary background processing once the widget is no longer visible or needed.


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

5. Time Formatting (_formatDuration)

This helper method takes a Duration object and converts it into a human-readable string format (HH:MM:SS or MM:SS if hours are zero). The padLeft(2, "0") ensures that single-digit numbers are padded with a leading zero for consistent formatting.


  String _formatDuration(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, "0");
    String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
    String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
    if (duration.inHours > 0) {
      return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
    }
    return "$twoDigitMinutes:$twoDigitSeconds";
  }

6. Building the User Interface (build)

The build method is responsible for rendering the UI. It uses a Container to apply optional padding and background color, and a Text widget to display the formatted _remainingDuration. Default styling is provided but can be overridden by the widget's parameters.


  @override
  Widget build(BuildContext context) {
    return Container(
      padding: widget.padding ?? const EdgeInsets.all(8.0),
      decoration: BoxDecoration(
        color: widget.backgroundColor ?? Colors.blueAccent,
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Text(
        _formatDuration(_remainingDuration),
        style: widget.textStyle ??
            const TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
              color: Colors.white,
            ),
      ),
    );
  }

How to Use the Widget

To integrate the CountdownTimerWidget into your application, simply instantiate it and pass the desired Duration. You can also provide an onTimerEnd callback to execute code when the countdown finishes.


import 'package:flutter/material.dart';
import 'countdown_timer_widget.dart'; // Assuming your widget is in this file

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Countdown Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

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

class _MyHomePageState extends State {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Countdown Timer Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Event starts in:',
              style: TextStyle(fontSize: 20),
            ),
            const SizedBox(height: 20),
            CountdownTimerWidget(
              duration: const Duration(minutes: 2, seconds: 30), // 2 minutes and 30 seconds
              onTimerEnd: () {
                // This callback fires when the timer reaches 0
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Countdown Finished!')),
                );
                print('Countdown has ended!');
              },
              textStyle: const TextStyle(
                fontSize: 48,
                fontWeight: FontWeight.w900,
                color: Colors.black87,
              ),
              backgroundColor: Colors.yellow.shade200,
              padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
            ),
            const SizedBox(height: 40),
            const Text(
              'Sale ends in:',
              style: TextStyle(fontSize: 20),
            ),
            const SizedBox(height: 20),
            CountdownTimerWidget(
              duration: const Duration(hours: 1, minutes: 5, seconds: 15), // 1 hour, 5 minutes, 15 seconds
              onTimerEnd: () {
                print('Sale countdown ended!');
              },
              textStyle: const TextStyle(
                fontSize: 36,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
              backgroundColor: Colors.redAccent,
            ),
          ],
        ),
      ),
    );
  }
}

Conclusion

Building a countdown timer widget in Flutter is a practical exercise that reinforces core Flutter concepts like StatefulWidget, lifecycle methods (initState, dispose), and asynchronous programming with dart:async's Timer class. By following this guide, you now have a flexible and reusable CountdownTimerWidget that can be easily integrated into various applications, enhancing user experience with dynamic time-based information.

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