Introduction
Animated gradients add motion and polish to Flutter apps with relatively little runtime cost when implemented correctly. This tutorial shows how to implement animated gradients using CustomPainter — giving you precise control of shader creation, animation parameters, and painting performance. We target Flutter for mobile development, but techniques apply to any Flutter platform.
Creating A CustomPainter
CustomPainter exposes a low-level paint callback where you can create shaders and draw shapes directly to the canvas. For animated gradients you typically create a Gradient shader in paint() and use canvas.drawRect or canvas.drawPath to fill the area. Keep painters stateless: pass the animation value in via the constructor and mark shouldRepaint to reflect changes.
Key points:
Use ui.Gradient (LinearGradient/RadialGradient) to generate a Shader via createShader(rect).
Limit allocations in paint(): reuse Rects, Paths, and Paint objects where possible and avoid creating large objects every frame.
Example paint method inside a CustomPainter:
import 'dart:ui' as ui;
@override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
final shader = ui.Gradient.linear(
rect.topLeft,
rect.bottomRight,
[Color(0xff3366ff), Color(0xff00ccff)],
[0.0, 1.0],
);
final paint = Paint()..shader = shader;
canvas.drawRect(rect, paint);
}That snippet shows a static linear gradient. To animate you’ll adjust the stops, colors, or the shader transform on each frame.
Animating The Gradient
Animation can be driven by an AnimationController in a StatefulWidget. Feed the animated value(s) into the CustomPainter so paint() can recompute the shader each frame. There are several animation strategies:
Interpolate between two sets of colors using Color.lerp and update the gradient’s colors.
Animate the gradient’s stops array to move color boundaries.
Apply a matrix transform to the shader using shader.toImage? (limited) — instead, shift endpoints or recompute shader with offset.
Keep the animation logic outside the painter and provide only the minimal state required. Here’s a compact example that shifts the gradient endpoints over time:
class AnimatedGradientWidget extends StatefulWidget {
@override
_AnimatedGradientWidgetState createState() => _AnimatedGradientWidgetState();
}
(See full sample projects for expanded implementations. The important pattern is: controller -> AnimatedBuilder -> CustomPaint(painter: MyPainter(value))).
Performance And Optimization
Painting on every frame can be cheap if you avoid allocations and complex operations. Follow these optimizations:
Reuse Paint and Rect objects as instance fields on the painter and only update shader and color properties per frame.
Limit the painted area. If only a header needs animation, restrict the CustomPaint size instead of painting the whole screen.
Use low-cost gradients. LinearGradient with a small number of color stops is faster than complex shaders.
Offload expensive precomputation (like generating intermediate palettes) to a separate isolate if it becomes a bottleneck.
Profile on device using Flutter DevTools to watch GPU and raster thread performance; adjust FPS and animation complexity accordingly for mobile development requirements.
shouldRepaint should compare the incoming animation value to the old one; avoid returning true unconditionally when unnecessary.
Integrating In Mobile UI
Use AnimatedBuilder or ValueListenableBuilder to minimize widget rebuilds and to keep repaint scope small. When placing a CustomPaint in a layout, set isComplex and willChange hints appropriately if you use RepaintBoundary. For example, wrap the animated gradient area with a RepaintBoundary to prevent raster work spilling into unrelated widgets.
For touch interactions, combine GestureDetector with a controller to pause or change direction of the animation. Make the gradient reactive to app state (dark mode, accessibility settings) by feeding theme colors into the painter.
Reusable painters are useful: design a small API (e.g., AnimatedGradientPainter({double t, List colors, bool horizontal})) so you can reuse the same painter with different parameters across screens.
Vibe Studio

Vibe Studio, powered by Steve’s advanced AI agents, is a revolutionary no-code, conversational platform that empowers users to quickly and efficiently create full-stack Flutter applications integrated seamlessly with Firebase backend services. Ideal for solo founders, startups, and agile engineering teams, Vibe Studio allows users to visually manage and deploy Flutter apps, greatly accelerating the development process. The intuitive conversational interface simplifies complex development tasks, making app creation accessible even for non-coders.
Conclusion
Implementing animated gradients with CustomPainter gives you pixel-level control for expressive, efficient visuals in Flutter mobile development. Keep animation logic separate, minimize allocations in paint(), and adjust complexity for device performance. With careful reuse of Paint/Rect and targeted repaint boundaries, animated gradients become a performant part of your UI toolkit. Start simple—animate stops or endpoints first—then layer more complexity only when you profile and confirm capability.