CustomPainter — what it is, paint and shouldRepaint methods in Flutter

Author: IT Sectr Published: 2026-07-22 Reading time: 10 min

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 — an abstract Flutter class that defines custom rendering on Canvas via the paint and shouldRepaint methods
  • CustomPaint — a widget that accepts CustomPainter as the painter or foregroundPainter parameter and displays the result on screen
  • Canvas — an object that provides drawing methods (drawLine, drawCircle, drawPath, drawRect, drawArc, and others)
  • shouldRepaint — an optimization method that determines whether the object needs to be redrawn on update
  • size — the size of the drawing area passed to paint(Canvas canvas, Size size) from the parent widget

What is CustomPainter?

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 and Paint in Flutter

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 MethodPurposeParameters
drawCircleDraws a circleOffset center, double radius, Paint paint
drawLineDraws a line segmentOffset p1, Offset p2, Paint paint
drawRectDraws a rectangleRect rect, Paint paint
drawPathDraws an arbitrary pathPath path, Paint paint
drawArcDraws an arc or sectorRect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint
drawRRectRounded rectangleRRect rrect, Paint paint
drawOvalDraws an ovalRect rect, Paint paint
drawImageDraws an image from ImageImage image, Offset p, Paint paint

Canvas Transformations

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.

The paint and shouldRepaint Methods

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.

Custom Drawing Example with CustomPainter in Dart

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.

dart
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;
    }
}

CustomPainter with Path for Complex Shapes

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.

dart
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;
    }
}

ForegroundPainter vs backgroundPainter: The Difference

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.

CustomPainter Performance Optimization

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

What is the difference between CustomPainter and Canvas in Flutter?

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.

Can CustomPainter be used without CustomPaint?

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.

How to animate a CustomPainter?

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.

Why does CustomPainter not update when data changes?

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.

Can CustomPainter be used for animation in ReorderableListView?

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

  • CustomPainter — an abstract Flutter class for custom 2D rendering with paint and shouldRepaint methods
  • CustomPaint — a widget that accepts painter (background) and foregroundPainter (foreground) for displaying graphics
  • Canvas — an object with drawing methods: drawLine, drawCircle, drawPath, drawRect, drawArc, and others
  • Paint — a class defining the drawing style: color, thickness, style (fill/stroke), opacity, and blend mode
  • shouldRepaint — an optimization method that returns true only when fields affecting the rendering change
  • Creating objects inside paint is forbidden — all Paint, Path, Rect must be cached for 60 fps
  • RepaintBoundary — a widget for isolating CustomPainter from redraws of the upstream widget tree

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.

Discuss the project

Read also