skip to content

Canvas Painting

CustomPaint hands a CustomPainter a Canvas and a Size to draw paths, shapes and clips, and shouldRepaint decides when to redraw. Interviewers ask how to repaint on animation without a rebuild.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Flutter, when is CustomPainter.shouldRepaint called, what should it return, and what goes wrong with always true or always false?

level: middleimportance: must knowfreq 50%

answer

  1. only when a new instance arrives
  2. compare fields with oldDelegate
  3. same instance: not called
  4. paint may run anyway
  5. false forever: stale drawing

basics

~20 s

shouldRepaint runs when a new painter instance replaces the old one, typically after a rebuild. Return true only if the new painter would draw something different, by comparing its fields with oldDelegate's. Always true wastes repaints; always false leaves stale drawings.

solid answer

~50 s

When a rebuild gives `CustomPaint` a new painter object, `RenderCustomPaint` compares it with the old one. If the old one is null or the runtime types differ, it repaints without asking; otherwise it calls `newPainter.shouldRepaint(oldPainter)` and repaints only on `true`. If the **same instance** is passed again, nothing is called at all. So `shouldRepaint` should compare the fields that affect drawing: `oldDelegate.progress != progress || oldDelegate.color != color`. Returning **always `true`** is safe but repaints on every parent rebuild. Returning **always `false`** means new data never shows until something else, such as a size change or a neighbour's repaint, forces a paint. Two subtleties: `paint` may run even when `shouldRepaint` said `false`, so it must not depend on being skipped; and comparing a mutable list by identity misses in-place changes. Changes that are not tied to rebuilds belong in the `repaint` listenable.

go deeper

for a junior

Remember that shouldRepaint compares the new painter with the old one and returns true only when the drawing would change.

for a middle

Explain exactly when it is called, why always true and always false both fail, and why paint must tolerate running anyway.

for a senior

Design painter inputs as immutable or versioned, route high-frequency changes through the repaint listenable, and catch identity comparisons of mutable collections in review.

for a principal

Set painter conventions for the codebase, such as immutable inputs and tested shouldRepaint, so custom graphics stay correct and cheap as teams extend them.

## What the method is for A `CustomPainter` is usually created inside a `build` method: `CustomPaint(painter: ChartPainter(points: points))`. Every rebuild creates a **new painter instance**. Repainting on every rebuild would waste work when the data did not change, so the framework asks the new painter whether it differs from the old one. That question is `shouldRepaint(covariant CustomPainter oldDelegate)`. ## Exactly when it runs `RenderCustomPaint`'s painter setter and its update logic decide: 1. **Same instance passed again**: the setter returns early. Neither `shouldRepaint` nor a repaint happens. 2. **New painter, no previous painter**: repaint, no question asked. 3. **New painter of a different runtime type**: repaint, no question asked. 4. **New painter of the same type**: call `newPainter.shouldRepaint(oldPainter)`; repaint only if it returns `true`. `shouldRebuildSemantics` follows the same pattern for the semantics tree and, by default, delegates to `shouldRepaint`. ## What to return Compare the **inputs that affect the drawing**: ```dart @override bool shouldRepaint(ChartPainter oldDelegate) => oldDelegate.points != points || oldDelegate.lineColor != lineColor || oldDelegate.showGrid != showGrid; ``` The parameter can be narrowed to your own type thanks to the `covariant` keyword in the base signature, so no cast is needed. ## Why "always true" and "always false" both fail | Choice | What happens | |---|---| | always `true` | correct, but every rebuild of any ancestor re-runs `paint`, which is wasteful for expensive drawings | | always `false` | new data passed through a rebuild is not drawn; the old picture stays until something else forces a paint | | field comparison | repaints exactly when the drawing would change | The "always false" bug is sneaky because it appears to work sometimes: if the widget's **size** changes, or an ancestor in the same layer repaints, `paint` runs anyway and the new data appears. The framework docs say so explicitly: `paint` may be called even when `shouldRepaint` returned `false`, and may be called without `shouldRepaint` at all, for example after a size change. ## The mutable-data trap A Dart `List`'s `==` is **identity**: two different lists with equal elements are not `==`, and the same list is always `==` to itself. Two common failure modes: - **Mutating the same list in place** and passing it to a new painter: `oldDelegate.points != points` is `false` because it is the same list, so the change is not drawn. - **Building a fresh list on every build** even when nothing changed: the comparison is always `true`, so every rebuild repaints. Fixes: - treat painter inputs as **immutable** and create a new list only when the data changes; - or keep a **version number** or timestamp beside the data and compare that; - or, for data that changes on its own schedule, stop routing it through rebuilds and use the **`repaint` listenable** instead. ## shouldRepaint vs the repaint listenable - `shouldRepaint` handles changes that arrive via **rebuilds**, when a new painter instance is created. - The `repaint` argument handles changes that arrive **without** rebuilds: an animation, a stream of samples, a scroll offset. The render object listens to it and calls `markNeedsPaint` directly, skipping build and layout. A painter driven purely by `repaint` usually returns `false` from `shouldRepaint`, or compares only the configuration fields (such as colours) that come from rebuilds. ## Testing it Because `shouldRepaint` is a pure function of two painter instances, it is one of the easiest things in a Flutter UI to unit-test: build an old and a new painter with the same data and expect `false`, change one field and expect `true`. A test per drawing input catches the "added a field, forgot to compare it" regression, which otherwise shows up only as a chart that stops updating after a refactor. `shouldRebuildSemantics` follows the same contract for the semantics tree and defaults to `shouldRepaint`; override it only when the painter's semantics depend on different fields than its pixels. ## Checklist - `paint` is a pure function of the painter's fields and `size`. - `shouldRepaint` compares exactly those fields. - Painter inputs are immutable, or versioned. - High-frequency updates use `repaint`, not `setState`.

  • Why might a painter whose shouldRepaint always returns false still show new data sometimes?
    Because `paint` can run for other reasons: the box changed size, or something else in the same layer repainted and re-ran every painter in it. The docs say `paint` may be called even when `shouldRepaint` returned `false`. That makes the bug look intermittent: data updates appear on rotation or during unrelated animations.
  • Is shouldRepaint called when the same painter instance is passed to CustomPaint again?
    No. `RenderCustomPaint`'s painter setter returns early when the new value is identical to the current one, so neither `shouldRepaint` nor a repaint happens. Reusing a painter instance is fine only if its drawing inputs arrive through the `repaint` listenable.
  • How does the covariant keyword help in shouldRepaint?
    The base method is declared as `shouldRepaint(covariant CustomPainter oldDelegate)`, which allows an override to narrow the parameter to its own type, such as `shouldRepaint(ChartPainter oldDelegate)`, and read its fields directly without a cast.

shouldRepaint is like a printer asking "is this the same document as last time?" before printing a new copy. Answering "always yes, reprint" wastes paper; answering "never reprint" means the updated document never comes out, unless someone jams the printer and forces a fresh print anyway.

saying these in an interview costs you the question

  • shouldRepaint runs every frame to decide whether to paint.
  • Returning false from shouldRepaint guarantees paint will not run.
  • Returning true from shouldRepaint is required for animations to work.
  • Comparing two mutable lists with != detects in-place changes.
  • shouldRepaint is called even when the identical painter instance is reused.
open as a page

In a Flutter voice-memo app, how do you repaint a live audio waveform on every new amplitude sample without rebuilding the widget tree?

level: middleimportance: must knowfreq 45%

basics

~10 s

Keep the samples in a ChangeNotifier owned by the State and pass it to the painter's super(repaint: ...). Each notifyListeners makes RenderCustomPaint call markNeedsPaint, so only paint runs: no setState, no build, no layout.

open as a page

In Flutter, how do the CustomPaint widget and a CustomPainter work together to draw custom graphics, and what must a CustomPainter implement?

level: juniorimportance: should knowfreq 45%

basics

~20 s

CustomPaint is the widget that sizes and hosts the drawing; a CustomPainter is the delegate that draws. A painter implements paint(Canvas, Size) to draw inside that size, and shouldRepaint(oldDelegate) to say whether a new instance needs a repaint.

open as a page

In Flutter, why might a CustomPaint draw nothing or spill outside its area, and how do size, clipping and save/restore fix it?

level: middleimportance: should knowfreq 35%

basics

~20 s

Without a child, CustomPaint asks for its size argument, Size.zero by default, so loose constraints give the painter an empty box. Drawing outside size is not reliably clipped, so clip to Offset.zero & size, and keep save and restore balanced.

open as a page

In Flutter, how do you use a custom GLSL fragment shader in a CustomPainter, from pubspec declaration to setting uniforms each frame?

level: seniorimportance: nice to knowfreq 15%

basics

~20 s

List the .frag file under flutter: shaders: in pubspec.yaml, load it once with FragmentProgram.fromAsset, create a FragmentShader, set uniforms with setFloat in declaration order, and draw with Paint()..shader = shader in paint. The tool compiles the shader at build time.

open as a page