check_for_equals_in_render_object_setters
Flags a RenderObject setter that calls a mark-dirty method without comparing the incoming value first.
updateRenderObject runs on every rebuild and assigns every property, whether or not it differs. An unguarded setter therefore turns each rebuild into a full relayout or repaint of that subtree, even when nothing about it changed. At best that is wasted frames on a hot path; when the layout pass itself causes another rebuild, the two feed each other and the app stops rendering.
Flutter’s own render objects all use the same shape: compare, then assign and mark dirty.
This rule is in the opinionated preset, so it is on with preset: opinionated and preset: pedantic.
See also: Flutter: RenderObject, RenderObjectWidget.updateRenderObject
class RenderBadge extends RenderBox { RenderBadge(this._color);
Color _color;
set color(Color value) { _color = value; markNeedsPaint(); // repaints on every rebuild, unchanged or not }}The early return is the idiomatic form:
class RenderBadge extends RenderBox { RenderBadge(this._color);
Color _color;
set color(Color value) { if (_color == value) return; _color = value; markNeedsPaint(); }}The wrapping form works just as well:
set color(Color value) { if (_color != value) { _color = value; markNeedsPaint(); }}Layout and paint are separate decisions
Section titled “Layout and paint are separate decisions”A property that changes the object’s size needs markNeedsLayout; one that only changes how it draws needs markNeedsPaint. Both need the same guard:
class RenderBadge extends RenderBox { RenderBadge(this._padding, this._color);
double _padding; Color _color;
set padding(double value) { if (_padding == value) return; _padding = value; markNeedsLayout(); // size changed }
set color(Color value) { if (_color == value) return; _color = value; markNeedsPaint(); // only the painting changed }}The recognised methods are markNeedsLayout, markNeedsPaint, markNeedsCompositingBitsUpdate, markNeedsSemanticsUpdate and markNeedsLayoutForSizedByParentChange.
Teaching it about a project wrapper
Section titled “Teaching it about a project wrapper”If your render objects mark themselves dirty through a helper of your own, name it so the rule keeps working:
many_lints: rules: check_for_equals_in_render_object_setters: additional_methods: [markNeedsCustomPass]rules: check_for_equals_in_render_object_setters: additional_methods: [markNeedsCustomPass]// Reported under the config aboveset weight(double value) { _weight = value; markNeedsCustomPass();}Known limitations
Section titled “Known limitations”Guard detection is deliberately loose. Any ==, != or identical call anywhere in the setter body counts as a guard, even one comparing something else entirely. A false positive on a setter that is in fact guarded would be far more annoying than missing an exotic shape.
Only setters that mark dirty are considered. A setter that merely assigns has nothing to guard against, and is never reported.
Only a block body is examined. An expression-bodied setter is skipped.
No quick fix. Which field to compare — and whether an unchanged value should really skip the work — is a decision about the render object.
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
additional_methods |
list of strings | [] |
Extra methods treated as marking the render object dirty |
Turning this rule off
Section titled “Turning this rule off”rules: check_for_equals_in_render_object_setters: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Related rules
Section titled “Related rules”always_pass_global_key— Don’t create a GlobalKey inside build.avoid_conditional_hooks— Never call hooks inside conditionals, loops, or ternaries.avoid_deep_widget_nesting— Keep a widget tree within a nesting budget.avoid_flexible_outside_flex— Only use Flexible and Expanded as direct children of Row, Column, or Flex.