In Flutter, when is CustomPainter.shouldRepaint called, what should it return, and what goes wrong with always true or always false?
answer
- only when a new instance arrives
- compare fields with oldDelegate
- same instance: not called
- paint may run anyway
- false forever: stale drawing
basics
~20 sshouldRepaint 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 sWhen 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
Remember that shouldRepaint compares the new painter with the old one and returns true only when the drawing would change.
Explain exactly when it is called, why always true and always false both fail, and why paint must tolerate running anyway.
Design painter inputs as immutable or versioned, route high-frequency changes through the repaint listenable, and catch identity comparisons of mutable collections in review.
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.