image

10 Dec 2025

9K

35K

Animated Progress Bar Widget in Flutter

Introduction

Progress bars are an essential UI element for indicating the status of a task, such as file uploads, downloads, or data processing. While a static progress bar conveys information, an animated one significantly enhances user experience by providing a smoother, more engaging visual feedback. In Flutter, creating such an animated widget is straightforward using its powerful animation framework.

This article will guide you through building a reusable animated progress bar widget in Flutter, covering the core animation concepts and practical implementation steps.

Core Animation Concepts

Before diving into the code, let's briefly review the key Flutter animation components we'll be using:

  • AnimationController: This class manages an animation. It can start, stop, forward, or reverse an animation, and also set its duration. It requires a TickerProvider (typically provided by SingleTickerProviderStateMixin or TickerProviderStateMixin) to synchronize the animation with the screen refresh rate.
  • Tween: A Tween (short for "between") defines a range of values over which an animation should occur. For instance, a Tween(begin: 0.0, end: 1.0) will animate a double value from 0.0 to 1.0.
  • Animation: An Animation object holds the current value of an animation. A Tween can generate an Animation by calling its animate() method with an AnimationController.
  • addListener() and setState(): To update the UI as the animation progresses, you attach a listener to the Animation. Inside the listener, setState() is called to rebuild the widget tree with the new animation value.
  • didUpdateWidget(): This lifecycle method is crucial for handling changes to the widget's properties (like the progress value) and ensuring the animation correctly updates from its current state to the new target state.
  • SingleTickerProviderStateMixin: A mixin that provides a single Ticker for an AnimationController. It's typically used with StatefulWidget.

Step-by-Step Implementation

1. Create the Widget Structure

We'll start by defining a StatefulWidget named AnimatedProgressBar. This widget will accept parameters for its current progress, colors, height, border radius, and animation duration, making it highly customizable.


import 'package:flutter/material.dart';

class AnimatedProgressBar extends StatefulWidget {
  final double progress; // Value from 0.0 to 1.0
  final Color backgroundColor;
  final Color progressBarColor;
  final double height;
  final BorderRadiusGeometry borderRadius;
  final Duration animationDuration;

  const AnimatedProgressBar({
    Key? key,
    required this.progress,
    this.backgroundColor = Colors.grey,
    this.progressBarColor = Colors.blue,
    this.height = 10.0,
    this.borderRadius = const BorderRadius.all(Radius.circular(5.0)),
    this.animationDuration = const Duration(milliseconds: 400),
  }) : assert(progress >= 0.0 && progress <= 1.0, 'Progress must be between 0.0 and 1.0'),
       super(key: key);

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

class _AnimatedProgressBarState extends State
    with SingleTickerProviderStateMixin {
  // AnimationController and Animation will be initialized here
  late AnimationController _animationController;
  late Animation _animation;
  double _currentAnimationProgress = 0.0; // Stores the progress value at the start of an animation cycle

  @override
  void initState() {
    super.initState();
    // Initialize _currentAnimationProgress with the initial progress
    _currentAnimationProgress = widget.progress; 

    _animationController = AnimationController(
      vsync: this,
      duration: widget.animationDuration,
    );

    _animation = Tween(begin: _currentAnimationProgress, end: widget.progress).animate(_animationController)
      ..addListener(() {
        setState(() {}); // Rebuild widget on every animation frame
      });

    // Start the initial animation to the provided progress
    _animationController.forward(from: 0.0);
  }

  // ... rest of the state class
}

2. Initialize Animation Controller and Animation

In the _AnimatedProgressBarState class, we need to initialize our AnimationController and Animation within the initState method. We also use SingleTickerProviderStateMixin to provide the necessary ticker for the controller.

The _currentAnimationProgress variable is introduced to keep track of the progress value from which the next animation should start. This ensures smooth transitions even if the progress is updated multiple times.


// ... (inside _AnimatedProgressBarState class)

  @override
  void initState() {
    super.initState();
    _currentAnimationProgress = widget.progress;

    _animationController = AnimationController(
      vsync: this,
      duration: widget.animationDuration,
    );

    _animation = Tween(begin: _currentAnimationProgress, end: widget.progress).animate(_animationController)
      ..addListener(() {
        setState(() {});
      });

    _animationController.forward(from: 0.0); // Start the initial animation
  }

// ...

3. Handle Progress Updates

When the progress property of the AnimatedProgressBar changes, we need to re-animate the bar from its current animated position to the new target progress. This is handled in the didUpdateWidget method.


// ... (inside _AnimatedProgressBarState class)

  @override
  void didUpdateWidget(covariant AnimatedProgressBar oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.progress != oldWidget.progress) {
      _currentAnimationProgress = _animation.value; // Get the current animated value
      _animation = Tween(begin: _currentAnimationProgress, end: widget.progress)
          .animate(_animationController);
      _animationController.forward(from: 0.0); // Restart animation from 0.0 (relative to current position)
    }
  }

// ...

4. Build the User Interface

The UI for the progress bar will consist of a Stack. The bottom layer will be a Container representing the background of the bar. On top of it, another Container wrapped in a FractionallySizedBox will represent the actual progress. The widthFactor of the FractionallySizedBox will be driven by our _animation.value.

ClipRRect is used to ensure that the progress bar respects the provided borderRadius, as Stack children might overflow without it.


// ... (inside _AnimatedProgressBarState class)

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: widget.borderRadius,
      child: Container(
        height: widget.height,
        child: Stack(
          children: [
            // Background bar
            Container(
              decoration: BoxDecoration(
                color: widget.backgroundColor,
                borderRadius: widget.borderRadius, // Apply to background for consistency
              ),
            ),
            // Progress bar
            FractionallySizedBox(
              widthFactor: _animation.value, // Use the animated value for width
              child: Container(
                decoration: BoxDecoration(
                  color: widget.progressBarColor,
                  borderRadius: widget.borderRadius, // Apply to progress bar
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

// ...

5. Clean Up Resources

It's crucial to dispose of the AnimationController when the widget is removed from the widget tree to prevent memory leaks.


// ... (inside _AnimatedProgressBarState class)

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

// ...

Full Widget Code

Here's the complete code for the AnimatedProgressBar widget:


import 'package:flutter/material.dart';

class AnimatedProgressBar extends StatefulWidget {
  final double progress; // Value from 0.0 to 1.0
  final Color backgroundColor;
  final Color progressBarColor;
  final double height;
  final BorderRadiusGeometry borderRadius;
  final Duration animationDuration;

  const AnimatedProgressBar({
    Key? key,
    required this.progress,
    this.backgroundColor = Colors.grey,
    this.progressBarColor = Colors.blue,
    this.height = 10.0,
    this.borderRadius = const BorderRadius.all(Radius.circular(5.0)),
    this.animationDuration = const Duration(milliseconds: 400),
  }) : assert(progress >= 0.0 && progress <= 1.0, 'Progress must be between 0.0 and 1.0'),
       super(key: key);

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

class _AnimatedProgressBarState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation _animation;
  double _currentAnimationProgress = 0.0;

  @override
  void initState() {
    super.initState();
    _currentAnimationProgress = widget.progress;

    _animationController = AnimationController(
      vsync: this,
      duration: widget.animationDuration,
    );

    _animation = Tween(begin: _currentAnimationProgress, end: widget.progress).animate(_animationController)
      ..addListener(() {
        setState(() {});
      });

    _animationController.forward(from: 0.0);
  }

  @override
  void didUpdateWidget(covariant AnimatedProgressBar oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.progress != oldWidget.progress) {
      _currentAnimationProgress = _animation.value;
      _animation = Tween(begin: _currentAnimationProgress, end: widget.progress)
          .animate(_animationController);
      _animationController.forward(from: 0.0);
    }
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: widget.borderRadius,
      child: Container(
        height: widget.height,
        child: Stack(
          children: [
            // Background bar
            Container(
              decoration: BoxDecoration(
                color: widget.backgroundColor,
                borderRadius: widget.borderRadius,
              ),
            ),
            // Progress bar
            FractionallySizedBox(
              widthFactor: _animation.value,
              child: Container(
                decoration: BoxDecoration(
                  color: widget.progressBarColor,
                  borderRadius: widget.borderRadius,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

How to Use the Widget

To use the AnimatedProgressBar, simply instantiate it in your widget tree and update its progress property in a setState call. The animation will automatically handle the smooth transition.


import 'package:flutter/material.dart';
// Don't forget to import your AnimatedProgressBar widget file
// import 'path/to/animated_progress_bar.dart'; 

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

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

class _ProgressBarDemoState extends State {
  double _downloadProgress = 0.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Animated Progress Bar Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedProgressBar(
              progress: _downloadProgress,
              height: 20.0,
              progressBarColor: Colors.deepPurple,
              backgroundColor: Colors.deepPurple.shade100,
              borderRadius: BorderRadius.circular(10.0),
              animationDuration: const Duration(milliseconds: 600),
            ),
            const SizedBox(height: 40),
            Text(
              'Progress: ${(_downloadProgress * 100).toStringAsFixed(0)}%',
              style: const TextStyle(fontSize: 24),
            ),
            const SizedBox(height: 40),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  // Simulate progress update
                  _downloadProgress += 0.1;
                  if (_downloadProgress > 1.0) _downloadProgress = 0.0;
                });
              },
              child: const Text('Update Progress'),
            ),
          ],
        ),
      ),
    );
  }
}

// To run this demo, ensure your main.dart calls ProgressBarDemo:
// void main() {
//   runApp(MaterialApp(home: ProgressBarDemo()));
// }

Customization Options

The AnimatedProgressBar widget offers several parameters for customization:

  • progress (required): The current progress value as a double between 0.0 and 1.0.
  • backgroundColor: The color of the background portion of the bar. Defaults to Colors.grey.
  • progressBarColor: The color of the filled portion of the bar. Defaults to Colors.blue.
  • height: The height of the progress bar. Defaults to 10.0.
  • borderRadius: The border radius for the entire bar, making it rounded or square. Defaults to Radius.circular(5.0).
  • animationDuration: The duration of the progress animation. Defaults to 400 milliseconds.

Conclusion

Creating an animated progress bar in Flutter is an excellent way to practice and understand Flutter's animation framework. By leveraging AnimationController, Tween, and proper lifecycle management, we've built a robust and customizable widget that provides smooth visual feedback to users. You can further enhance this widget by adding text overlays, different animation curves, or even custom painting for more complex designs.

Related Articles

Dec 19, 2025

Flutter & Firebase Auth: Seamless Social Media Login

Flutter & Firebase Auth: Seamless Social Media Login In today's digital landscape, user authentication is a critical component of almost every application. Pro

Dec 19, 2025

Building a Widget List with Sticky

Building a Widget List with Sticky Header in Flutter Creating dynamic and engaging user interfaces is crucial for modern applications. One common UI pattern th

Dec 19, 2025

Mastering Transform Scale & Rotate Animations in Flutter

Mastering Transform Scale & Rotate Animations in Flutter Flutter's powerful animation framework allows developers to create visually stunning and highly intera