image

21 Mar 2026

9K

35K

Crafting a Dynamic Gradient Rotating Loader in Flutter

Creating engaging user interfaces is crucial for a positive user experience. While Flutter provides excellent default progress indicators, custom loaders can significantly enhance the visual appeal and brand identity of an application. This article will guide you through building a beautiful, animated rotating loader with a gradient color effect using Flutter's custom painting and animation capabilities.

Understanding the Core Components

To achieve our gradient rotating loader, we'll leverage three primary Flutter features:

  • CustomPainter: This allows us to draw custom shapes and paths directly onto the canvas, forming the basis of our loader's arc.
  • ShaderMask: An incredibly powerful widget that applies a shader (like a gradient) to its child. This is how we'll introduce the vibrant gradient colors to our loader.
  • AnimationController and RotationTransition: These are Flutter's tools for creating and managing animations. AnimationController drives the animation, and RotationTransition provides an easy way to apply continuous rotation to a widget.

Step-by-Step Implementation

1. The Loader Arc with CustomPainter

First, we need a custom painter to draw the basic arc shape of our loader. This painter will draw a simple arc, and its color will later be masked by our gradient.


import 'package:flutter/material.dart';
import 'dart:math' as math;

class MyArcPainter extends CustomPainter {
  final double sweepAngle;
  final double strokeWidth;

  MyArcPainter({this.sweepAngle = math.pi * 1.5, this.strokeWidth = 10.0});

  @override
  void paint(Canvas canvas, Size size) {
    final rect = Rect.fromLTWH(0, 0, size.width, size.height);
    final paint = Paint()
      ..color = Colors.white // This color will be masked by ShaderMask
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    // Draw the arc, starting from the top (-math.pi / 2)
    canvas.drawArc(rect, -math.pi / 2, sweepAngle, false, paint);
  }

  @override
  bool shouldRepaint(covariant MyArcPainter oldDelegate) {
    return oldDelegate.sweepAngle != sweepAngle || oldDelegate.strokeWidth != strokeWidth;
  }
}

2. Applying Gradient Colors with ShaderMask

ShaderMask will wrap our CustomPaint widget. It takes a shaderCallback which provides the bounds of the widget, allowing us to create a LinearGradient that fills those bounds.


// Inside the build method of our main loader widget...
ShaderMask(
  shaderCallback: (bounds) {
    return LinearGradient(
      colors: widget.gradientColors, // Colors passed to the widget
      tileMode: TileMode.mirror,
    ).createShader(bounds);
  },
  child: CustomPaint(
    painter: MyArcPainter(
      sweepAngle: math.pi * 1.5, // A 270-degree arc
      strokeWidth: widget.strokeWidth,
    ),
  ),
)

3. Animating the Rotation

We'll use an AnimationController to drive a continuous rotation. The RotationTransition widget takes this controller and applies a rotation transformation to its child.


// In a StatefulWidget's State class
class _GradientRotatingLoaderState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: widget.duration, // Duration passed to the widget
    )..repeat(); // Make the animation repeat indefinitely
  }

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

  @override
  Widget build(BuildContext context) {
    return RotationTransition(
      turns: _controller, // Uses the controller for rotation
      child: // ... our ShaderMask and CustomPaint here
    );
  }
}

4. Assembling the GradientRotatingLoader Widget

Now, let's combine all these pieces into a reusable StatefulWidget called GradientRotatingLoader.


import 'package:flutter/material.dart';
import 'dart:math' as math;

// MyArcPainter class goes here (from step 1)

class GradientRotatingLoader extends StatefulWidget {
  final double size;
  final double strokeWidth;
  final Duration duration;
  final List gradientColors;

  const GradientRotatingLoader({
    Key? key,
    this.size = 100.0,
    this.strokeWidth = 10.0,
    this.duration = const Duration(seconds: 2),
    this.gradientColors = const [Colors.blue, Colors.cyan],
  }) : super(key: key);

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

class _GradientRotatingLoaderState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: widget.duration,
    )..repeat(); // Repeat indefinitely
  }

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

  @override
  Widget build(BuildContext context) {
    return RotationTransition(
      turns: _controller,
      child: SizedBox(
        width: widget.size,
        height: widget.size,
        child: ShaderMask(
          shaderCallback: (bounds) {
            return LinearGradient(
              colors: widget.gradientColors,
              tileMode: TileMode.mirror,
            ).createShader(bounds);
          },
          child: CustomPaint(
            painter: MyArcPainter(
              sweepAngle: math.pi * 1.5, // 270 degrees arc
              strokeWidth: widget.strokeWidth,
            ),
          ),
        ),
      ),
    );
  }
}

Complete Example Code

Here's the full code for a basic Flutter application demonstrating our GradientRotatingLoader:


import 'package:flutter/material.dart';
import 'dart:math' as math;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.blueGrey[900],
        appBar: AppBar(
          title: const Text('Gradient Rotating Loader'),
          backgroundColor: Colors.blueGrey[800],
        ),
        body: Center(
          child: GradientRotatingLoader(
            size: 100,
            strokeWidth: 10,
            duration: const Duration(seconds: 2),
            gradientColors: const [
              Colors.purple,
              Colors.red,
              Colors.orange,
              Colors.yellow,
              Colors.green,
              Colors.blue,
              Colors.indigo,
            ],
          ),
        ),
      ),
    );
  }
}

class MyArcPainter extends CustomPainter {
  final double sweepAngle;
  final double strokeWidth;

  MyArcPainter({this.sweepAngle = math.pi * 1.5, this.strokeWidth = 10.0});

  @override
  void paint(Canvas canvas, Size size) {
    final rect = Rect.fromLTWH(0, 0, size.width, size.height);
    final paint = Paint()
      ..color = Colors.white // This color will be masked by ShaderMask
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    // Draw the arc, starting from the top (-math.pi / 2)
    canvas.drawArc(rect, -math.pi / 2, sweepAngle, false, paint);
  }

  @override
  bool shouldRepaint(covariant MyArcPainter oldDelegate) {
    return oldDelegate.sweepAngle != sweepAngle || oldDelegate.strokeWidth != strokeWidth;
  }
}

class GradientRotatingLoader extends StatefulWidget {
  final double size;
  final double strokeWidth;
  final Duration duration;
  final List gradientColors;

  const GradientRotatingLoader({
    Key? key,
    this.size = 100.0,
    this.strokeWidth = 10.0,
    this.duration = const Duration(seconds: 2),
    this.gradientColors = const [Colors.blue, Colors.cyan],
  }) : super(key: key);

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

class _GradientRotatingLoaderState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: widget.duration,
    )..repeat(); // Repeat indefinitely
  }

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

  @override
  Widget build(BuildContext context) {
    return RotationTransition(
      turns: _controller,
      child: SizedBox(
        width: widget.size,
        height: widget.size,
        child: ShaderMask(
          shaderCallback: (bounds) {
            return LinearGradient(
              colors: widget.gradientColors,
              tileMode: TileMode.mirror,
            ).createShader(bounds);
          },
          child: CustomPaint(
            painter: MyArcPainter(
              sweepAngle: math.pi * 1.5, // 270 degrees arc
              strokeWidth: widget.strokeWidth,
            ),
          ),
        ),
      ),
    );
  }
}

Code Breakdown

  • MyArcPainter

    This class extends CustomPainter and is responsible for drawing a partial circle (an arc). It takes sweepAngle to define the length of the arc and strokeWidth for its thickness. The paint method uses canvas.drawArc to render the arc. We set the Paint color to Colors.white, which acts as a placeholder that will be overridden by the ShaderMask.

  • GradientRotatingLoader Widget

    This is a StatefulWidget because it manages an AnimationController. It takes parameters like size, strokeWidth, duration (for animation speed), and gradientColors to make it highly customizable.

  • _GradientRotatingLoaderState

    • SingleTickerProviderStateMixin: This mixin is essential for `AnimationController` to prevent animations from consuming unnecessary resources when off-screen.
    • _controller: An `AnimationController` is initialized in `initState` with the specified `duration` and `vsync`. `..repeat()` ensures the animation loops continuously.
    • dispose(): It's crucial to `dispose` of the `AnimationController` when the widget is removed from the widget tree to prevent memory leaks.
    • build():
      • RotationTransition: This widget wraps the entire loader, using `_controller` to continuously rotate its child. The `turns` property takes an `Animation`, which our `AnimationController` provides directly.
      • SizedBox: Used to define the fixed dimensions (`size`) of our loader.
      • ShaderMask: This widget is the magic behind the gradient. The `shaderCallback` method receives the `bounds` of the widget and returns a `LinearGradient`'s shader. The gradient's colors are taken from `widget.gradientColors`.
      • CustomPaint: This is the child of `ShaderMask` and uses our `MyArcPainter` to draw the arc shape.

Conclusion

By combining CustomPainter for drawing, ShaderMask for vibrant gradients, and AnimationController with RotationTransition for smooth motion, you can create a highly customizable and visually appealing rotating loader in Flutter. This approach offers flexibility to change the arc shape, gradient colors, rotation speed, and even incorporate other animations, providing a unique touch to your application's user 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