Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

135. A frame is asked for by the value that moved

Date: 2026-08-19

Status

Accepted. Refines ADR-0128, and supersedes the @Model(repaint = false) it introduced.

Context

ADR-0128 made a change its own frame request and put the opt-out on the class:

@Model(repaint = false)
public final class BackgroundJob { … }

The granularity is wrong, and obviously so once a real model is written. One model routinely holds both the gain a slider shows and the byte counter nothing shows. A switch on the class has to be wrong about one of them, and the way out of being wrong is to split the model along a line that has nothing to do with what the model is about — which is the tail wagging the dog.

The showcase hit it immediately: added, the counter behind “Untitled 3”, is genuinely part of what the model knows and is displayed by nothing.

Decision

The value says whether showing it needs a frame.

@Bind("app.gain")                              Number gain = 40;    // asks
@Bind(value = "app.tabs-added", repaint = false) int added;         // does not

@Model(repaint = false) is gone. There is one place to look, and it is next to the value the question is about.

Decided in the build. The weaver emits the repainted() call in the setters of fields that ask and emits nothing in the others — so a value declared repaint = false costs an instruction that is not there rather than a branch that is. The runtime has no notion of which fields are quiet, and needs none.

It is not “do not observe”

A field declared repaint = false still notifies everything bound to it. Off means do not wake the window; something else may perfectly well be watching a value nothing on screen shows. There is a test for exactly that, because the two are easy to conflate and the conflation would be silent.

Consequences

The three signals a change can raise are now all per value and all declared in one place:

DeclaredFires
the binding@Bind("a.b")always, on a real change
a frameby default; off with repaint = falseafter the binding’s listeners
a restylerestyle = truebefore the binding’s listeners

Which reads as one idea rather than three, and is a better answer than the two places it took before.

Models.repaints(model) is gone, and with it the only thing that read @Model’s members at run time. @Model is now a bare marker again — still RUNTIME-retained, for the single purpose of telling an author their class was never woven.

A model with every field quiet still gets a repainted() listener list, empty and never fired. One null field on one object per model; not worth a switch.