CustomPainter is an abstract class from the Flutter SDK that gives developers full control over rendering custom 2D graphics on Canvas. A developer creates a subclass of CustomPainter and implements two required methods: paint(Canvas, Size) and shouldRepaint(covariant CustomPainter oldDelegate). According to the Flutter API Documentation (2026), CustomPainter is used together with the CustomPaint widget, which delegates rendering to the provided CustomPainter object on each animation frame or state change.
Key Takeaways
CustomPainter is a base Flutter class for implementing custom 2D graphics. It replaces the approach of overriding onDraw in Android by providing a more flexible and declarative rendering model. A developer creates a subclass of CustomPainter, overrides the paint method where they receive Canvas and Size, and performs all drawing operations on the Canvas. The CustomPaint widget manages the paint call and integrates the result into the widget tree.
The key difference between CustomPainter and direct drawing through Canvas is the separation of rendering logic (paint) and redraw trigger (shouldRepaint). The Flutter system calls shouldRepaint every time the parent widget changes. If shouldRepaint returns false, paint is not called, saving resources. This mechanism is similar to shouldRebuild in RenderObject and is part of the Flutter architecture with its three trees (Widget, Element, RenderObject).
CustomPaint is a widget that accepts the painter (background layer) and foregroundPainter (foreground layer) parameters. Both parameters accept CustomPainter objects. The background painter is drawn before the CustomPaint child widget, and the foregroundPainter is drawn after. This allows using CustomPaint as a container with decorative background or overlay without losing the ability to embed regular widgets inside.
Canvas is the central drawing object in Flutter, encapsulating the graphics surface. It provides about 40 drawing methods: drawLine, drawCircle, drawRect, drawPath, drawArc, drawImage, drawPicture, drawPoints, and others. Canvas in Flutter corresponds to a similar class in Android Canvas but is adapted to the Dart architecture and Flutter Engine. Canvas operates in a coordinate system where the origin (0, 0) is the top-left corner.
Paint — not to be confused with the paint() method — is a class that defines the drawing style: color, stroke width, style (fill or stroke), opacity, blend mode, and anti-aliasing mask. Paint is configured before drawing and passed to Canvas methods. Important: Paint is a mutable object, but it is recommended to create it once and reuse it, changing only the necessary parameters.
| Canvas Method | Purpose | Parameters |
|---|---|---|
| drawCircle | Draws a circle | Offset center, double radius, Paint paint |
| drawLine | Draws a line segment | Offset p1, Offset p2, Paint paint |
| drawRect | Draws a rectangle | Rect rect, Paint paint |
| drawPath | Draws an arbitrary path | Path path, Paint paint |
| drawArc | Draws an arc or sector | Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint |
| drawRRect | Rounded rectangle | RRect rrect, Paint paint |
| drawOval | Draws an oval | Rect rect, Paint paint |
| drawImage | Draws an image from Image | Image image, Offset p, Paint paint |
Canvas supports coordinate system transformations: translate(double dx, double dy) shifts the origin; rotate(double radians) rotates the system; scale(double sx, double sy) scales; skew(double sx, double sy) shears. Before a transformation, it is recommended to save the Canvas state via save() and restore it after completion via restore(). This prevents the accumulation of transformations in subsequent paint calls.
paint(Canvas canvas, Size size) is the required method that implements all drawing logic. The canvas parameter is the graphics surface for drawing. The size parameter is the size of the area allocated for drawing (width and height in logical pixels). The Canvas is pre-configured so that the origin (0,0) corresponds to the top-left corner of the drawing area. All coordinates are specified relative to this origin.
shouldRepaint(covariant CustomPainter oldDelegate) is a required method that optimizes redraw frequency. Flutter calls shouldRepaint on every widget tree update that affects the CustomPainter. If the method returns false, the already rendered image is considered valid and paint is not called. A typical implementation is: return oldDelegate.someField != someField — comparing the old and new painter fields to determine the need for redraw.
For animated or constantly changing images (clocks, loading indicators, charts) shouldRepaint should return true on every state change. If the object is static (logo, icon, background), shouldRepaint returns false after the first render. Flutter automatically caches the paint result and redraws only when the size, opacity, or shouldRepaint call changes.
Let us create GaugePainter — a custom circular gauge with animated fill. CustomPainter accepts the current value (progress from 0.0 to 1.0) and draws an arc with a gradient. The shouldRepaint method compares the progress value for optimization. CustomPaint is placed in the widget tree with the size parameter to limit the drawing area.
class GaugePainter extends CustomPainter {
final double progress;
final Color startColor;
final Color endColor;
GaugePainter({
required this.progress,
this.startColor = Colors.blue,
this.endColor = Colors.cyan,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2,
size.height / 2);
final radius = center.dx - 12;
final bgPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..style = PaintingStyle.stroke
..strokeWidth = 16
..strokeCap = StrokeCap.round;
final progressPaint = Paint()
..shader = LinearGradient(
colors: [startColor, endColor],
).createShader(
Rect.fromCircle(
center, radius))
..style = PaintingStyle.stroke
..strokeWidth = 16
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, bgPaint);
canvas.drawArc(
Rect.fromCircle(center, radius),
-math.pi / 2,
2 * math.pi * progress,
false, progressPaint);
}
@override
bool shouldRepaint(
GaugePainter oldDelegate) {
return oldDelegate.progress != progress;
}
}
The example draws a wave chart using Path. Path allows building arbitrary contours from lines, Bezier curves, and arcs. After building the contour, it is filled with color or outlined with a line. In the example, the chart is built from an array of points simulating a wave form.
class WavePainter extends CustomPainter {
final List<double> values;
WavePainter({required this.values});
@override
void paint(Canvas canvas, Size size) {
final path = Path();
final dx = size.width /
(values.length - 1);
path.moveTo(0, size.height);
for (var i = 0; i < values.length; i++) {
final x = i * dx;
final y = size.height *
(1 - values[i]);
path.lineTo(x, y);
}
path.lineTo(size.width, size.height);
path.close();
final fillPaint = Paint()
..color = Colors.blue.withOpacity(0.3)
..style = PaintingStyle.fill;
canvas.drawPath(path, fillPaint);
}
@override
bool shouldRepaint(
WavePainter oldDelegate) {
return oldDelegate.values != values;
}
}
painter (background) is drawn before the CustomPaint child widget. If CustomPaint has a child widget (the child parameter), the background painter is displayed beneath it. This is suitable for decorations: progress bars, background patterns, watermarks. foregroundPainter is drawn after the child widget, meaning on top of it. It is used for overlays: highlights, masks, measuring rulers, annotations.
The difference between painter and foregroundPainter is critical for Z-order. If you need to draw something behind the content — use painter. If on top of the content — foregroundPainter. In some cases, you can use both painters simultaneously: background via painter, overlay label via foregroundPainter. This allows separating the logic of different graphic layers into two separate CustomPainter classes.
The size parameter in the paint method is the same for both painters and is determined by the size of the area allocated for CustomPaint. The size can be set explicitly via the size parameter in CustomPaint or implicitly through the parent container. If the size is Size.zero, paint is not called — Flutter skips rendering for a zero-size area. This behavior should be considered when animating element appearance.
The main rule of CustomPainter performance is not to create objects inside the paint method. Canvas, Paint, Path, Rect, Offset, and other drawing objects should be created in the CustomPainter constructor or cached. Creating objects inside paint leads to allocations on every frame, which at 60 frames per second causes frequent garbage collection and animation jank.
Path caching significantly speeds up rendering of repeated complex paths. If the path does not change between frames (for example, a background grid), create the Path once and reuse it multiple times. RepaintBoundary is a wrapper widget that prevents CustomPainter redraw when the upstream widget tree changes. If the CustomPainter is static, wrap it in a RepaintBoundary for isolation.
For animated CustomPainter with frequent redraws, use Ticker or AnimationController with shouldRepaint returning true on each change of the animated value. It is optimal to compare the animated field in shouldRepaint rather than redrawing on every frame unnecessarily. Rect caching for complex gradients: if the gradient colors do not change, you can cache the Shader via Paint.shader.
Frequently Asked Questions
CustomPainter is a contract class that defines what and how to draw on the Canvas. Canvas is the drawing object itself, which provides methods (drawCircle, drawLine, etc.). CustomPainter receives Canvas through the paint method and controls its usage. One Canvas can be used by different CustomPainter objects at different times.
No, CustomPainter has no meaning without CustomPaint. CustomPaint is the widget that calls the CustomPainter paint method and displays the result on screen. Without CustomPaint, Canvas will not be created and paint will not execute. You can, however, implement your own RenderObject with Canvas, but this is a lower-level approach.
Use an AnimationController with a Ticker, passing the animated value to the CustomPainter through constructor parameters. When the value changes, call setState() in the widget containing CustomPaint, which triggers the shouldRepaint call. If shouldRepaint returns true, the system calls paint for the new animation frame.
The cause is almost always shouldRepaint returning false. Check whether the oldDelegate fields are correctly compared with the new values. If a mutable object is used, shouldRepaint may not detect changes because the object reference remains the same. Use == or immutable objects for correct comparison.
Yes, CustomPainter is used for rendering custom decorations in complex lists. However, note that when reordering items, the RepaintBoundary inside ListView caches each item rendering. CustomPainter should be lightweight and not contain heavy operations in paint to avoid slowing down the drag animation.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also