Widgets & state
A Fleury UI is a tree of widgets: immutable descriptions of what should be on screen. You never mutate a widget — you describe a new one and let the framework work out the minimal change. (If you’ve used Flutter, this is the same model, painting to a grid of cells instead of pixels; everything below will feel like home.)
Two kinds of widget
Section titled “Two kinds of widget”A StatelessWidget describes UI from its inputs and any state it reads
through its build context. It has no companion mutable State object. Override
one method, build, which returns what the widget displays:
class Greeting extends StatelessWidget { const Greeting(this.name, {super.key}); final String name;
@override Widget build(BuildContext context) => Text('Hello, $name');}A StatefulWidget also carries mutable state that survives rebuilds — a
counter, a scroll position, a text buffer. The widget itself is still immutable;
the mutable part lives in a companion State object that the framework keeps
alive across rebuilds:
Rebuild after changing state
Section titled “Rebuild after changing state”When a field in your State changes, use setState to rebuild its widget:
setState(() => count++);setState runs your callback synchronously, then marks this widget for rebuild.
On the next frame the framework re-runs the dirty build path, performs the
layout and paint work that change requires, and diffs the new cell grid against
the old. The terminal presenter writes only cells that actually changed; when
the runtime has no frame work, it skips build, layout, paint, and presentation.
For fields owned by your State, use setState to schedule the rebuild.
Shared models publish changes through Notifier.notify(); subscribe with
NotifierBuilder or context.listen(model) when your UI reads their state.
The State lifecycle
Section titled “The State lifecycle”The framework owns your State object’s life. The methods you’ll override
most, in the order they fire:
initState()— once, when the state is first inserted. Set up controllers, start subscriptions. Always callsuper.initState().didChangeDependencies()— right afterinitState, and again whenever an inherited dependency you read (aTheme, aMediaQuery) changes. Not called for a plainsetState.build(context)— whenever this state is marked dirty, an inherited dependency changes, or its parent supplies updated configuration. Keep it pure: no side effects, just describe the tree.didUpdateWidget(oldWidget)— when the parent rebuilds and hands this state a new widget instance of the same type. ComparewidgettooldWidgetand react: if the parent passed a different stream or notifier, unsubscribe from the old one and subscribe to the new. (Callbacks need nothing here; readwidget.onChangedwhen you call it.)dispose()— once, when the widget is removed for good. Tear down anything you started ininitState— controllers, tickers, stream subscriptions. Always callsuper.dispose().
A clock starts its timer in initState and cancels it in dispose:
Three more hooks cover rarer cases. deactivate() runs when the state leaves
the tree, and activate() runs if it’s reinserted in the same frame, which
happens when a widget with a GlobalKey moves to a new parent. reassemble()
runs after a hot reload, for recomputing cached values; see
Hot reload.
Inside a State you also have three getters: widget (the current
configuration), context (this widget’s location in the tree), and
mounted (whether the state is still in the tree — guard async callbacks
with if (!mounted) return; before calling setState).
BuildContext
Section titled “BuildContext”The BuildContext handed to build is a handle to where this widget sits in
the tree. Use it to read values provided by ancestors:
final theme = Theme.of(context); // nearest ThemeDatafinal size = MediaQuery.sizeOf(context); // terminal size, in cellsThese walk up the tree to find the nearest ancestor that provides the value, and
they subscribe this widget to it — change the theme and every widget that
read Theme.of(context) rebuilds. That’s the mechanism behind theming and
responsive layout; it’s a Scope under the hood (see below). There
are shorthands too: context.theme and context.colors.
For application state, context.scope<Model>() finds the nearest Scope<Model>
and subscribes this widget until it leaves the tree. If you already have a
model, context.listen(model) subscribes directly and returns that same
object; that subscription lasts while the widget’s builds keep reading the
model. Call these readers during this widget’s build, and use the captured
model in event callbacks. Both subscriptions end automatically when the widget
unmounts.
Note one difference from a render tree: a BuildContext has no .size. A widget
doesn’t know its own dimensions during build (it hasn’t been laid out yet).
Read the screen size from MediaQuery, and make a subtree adapt to its space
with layout widgets like Expanded and Wrap (see Layout).
Who owns a control’s value?
Section titled “Who owns a control’s value?”With value and onChanged, your state owns the value. The control asks for a
change, and you supply the updated value. Here region is a String field on
your State:
Select<String>( value: region, options: const [ SelectOption(value: 'us-east', label: 'US East'), SelectOption(value: 'eu-west', label: 'EU West'), ], onChanged: (value) => setState(() => region = value),)The select always shows region. Picking an option calls onChanged, and the
new choice appears only after your setState stores it. The
Select reference has a live example.
A controller holds live state that both your code and the widget can change.
Create it once in State and dispose it with that state:
final list = ListController(initialIndex: 24);
@overridevoid dispose() { list.dispose(); super.dispose();}The task browser
starts at task 25. Arrow keys and the Go to 25 button update the same
list.currentIndex; ordinary rebuilds preserve it.
An initial* widget argument seeds internal state once. For example,
NumberInput(initialValue: 42) keeps the user’s edits when its parent rebuilds.
Use a controller for later programmatic changes, or a new key for a deliberate
reset. Supply a seed or a controller for the same value, not both.
Input onChanged callbacks report edits from the user and edits made through
the semantic tree, such as a test’s fill or an agent setting the field’s
value. Programmatic controller writes notify controller listeners, so updating
a model does not echo through an input callback. Replacing a controller adopts
the new one’s state; omitting it creates fresh internally owned state.
When the framework rebuilds, it reuses existing State objects by matching each
new widget to the old one at the same position with the same type. Usually that’s
exactly right and you pass no key. You reach for a Key when identity needs
to survive reordering — most often a list whose items get inserted, removed, or
shuffled:
ValueKey(item.id)— ties a widget’s identity to a stable value, so its state follows it when the list reorders. The common case.UniqueKey()— equal only to itself; use it to force a fresh state (a remount) where you’d otherwise get reuse.GlobalKey()— unique across the whole tree; lets you reach a widget’sStatefrom elsewhere viakey.currentState. Powerful but heavier — prefer lifting state up before reaching for one.
Sharing data down the tree
Section titled “Sharing data down the tree”Use Scope(value, child: ...) to share an existing object with descendants, or
Scope.create(Model.new, child: ...) to create and own a model for a subtree.
Descendants choose context.scope<Model>() or a ScopeBuilder<Model> consumer.
The type argument is the key, and the nearest scope of that type wins.
Plain values notify when replaced by an unequal value; notifiers also publish
changes themselves. The State management guide
shows complete examples of both readers. See the
Scope reference for creation and disposal rules.
The built-ins you’ve met (Theme, MediaQuery, and DefaultTextStyle) use
scopes behind their .of(context) helpers. Give your own scope a distinct
value type when it needs an independent identity in the tree.
Next: where the tree starts running — App entry points. For arranging widgets once you have them, see Layout; for the leaf widgets that go in the tree, the widget reference.