State management
Every piece of state has an owner: the code that creates it, changes it, and cleans it up. Choose the owner by asking who needs the state:
| The state is used by… | Keep it in | Read it with |
|---|---|---|
| One widget | State, updated with setState | The state’s own fields |
| Widgets below one owner | The owner’s State, shared with Scope(value) | context.scope<T>() or ScopeBuilder<T> |
| Several widgets that also change it | A Notifier created by Scope.create | context.scope<T>() or ScopeBuilder<T> |
| Widgets and code outside the tree | A Notifier created outside the tree, shared with Scope(model) or passed to widgets | context.scope<T>(), context.listen, or NotifierBuilder |
Local state
Section titled “Local state”Use a StatefulWidget when a value belongs to one part of the UI. Keep it in
State and update it with setState:
Press Increment. setState schedules a rebuild, so the widget displays the
new value.
When a few nearby widgets need the value, keep it in their closest common parent and pass it down through constructors. Use a scope when it has to reach widgets further down.
Share a value with a subtree
Section titled “Share a value with a subtree”Scope shares a value with every widget below it. Here the screen owns a
Project and shares it; ProjectTitle and ProjectPath read it without any
constructor parameters:
Press Switch project. The screen replaces its value inside setState, and
both readers update. They are const, so rebuilding the screen does not
rebuild them: reading the scope subscribed each one to it.
The scope’s type is the lookup key, and it must match exactly. A reader finds
the nearest Scope<Project> above it, and an inner scope of the same type
overrides an outer one. Scope(value) infers its type from the value you pass,
so a subclass registers under its own type: if SampleProject extends
Project, Scope(SampleProject()) is a Scope<SampleProject>, and
context.scope<Project>() does not find it. Name the type when you share a
subtype, such as a test fake: Scope<Project>(SampleProject(), child: …).
The two readers differ only in what rebuilds. context.scope<Project>()
rebuilds the widget that calls it; ScopeBuilder<Project> rebuilds just its
builder, which helps when a large build method uses the value in one small
part. Read a scope in build, initState, or didChangeDependencies, and let
event handlers use the value read there. When the scope is optional, add ? to
the type: context.scope<Project?>() returns null when no Scope<Project> is
above.
Share a model that widgets change
Section titled “Share a model that widgets change”When several widgets change the same state, move it into a model that
announces its changes. Extend Notifier, update the fields, then call
notify():
class Cart extends Notifier { int _itemCount = 0;
int get itemCount => _itemCount;
void addItem() { _itemCount++; notify(); }}Let the part of the app that uses the cart own it. Scope.create creates the
model when the scope mounts, shares it with the subtree, and disposes it when
the scope leaves the tree:
Add coffee and tea. The badge and the buttons are separate widgets, and none of
them receives the cart through its constructor. Reading a scope whose value is
a Notifier also subscribes the reader to its notifications, so CartBadge
rebuilds after every notify().
Keep a model outside the tree
Section titled “Keep a model outside the tree”Create the model outside the widget tree when code other than widgets uses it:
a sync service, a background job, or the command that runs after the UI exits.
Whoever creates it disposes it. Scope(model) lends it to the widgets without
taking ownership, and the readers above do not change:
Future<void> main(List<String> args) async { final cart = Cart(); try { await runApp( FleuryApp( title: 'Shop', home: Scope(cart, child: const ShopScreen()), ), args: args, ); } finally { cart.dispose(); }}runApp completes after the UI has closed and the terminal is restored, so the
finally block disposes the cart once no widget can read it. Under a plain
dart run, code before runApp also runs in the hot-reload supervisor process;
before starting a service there, see
Keep startup work inside the app.
Listen to a model directly
Section titled “Listen to a model directly”A widget that already holds a model, for example from its constructor, can
listen to it without a scope. NotifierBuilder rebuilds its builder;
context.listen(model) rebuilds the calling widget and returns the same model.
Both readers below share one cart: add an item in either and both counts update.
Edit the NotifierBuilder or context.listen tab to change its matching
reader:
The Shared cart tab creates that cart in State and passes it to each
reader’s constructor. The Cart tab is the model from above.
NotifierBuilder listens while it is in the tree. context.listen listens
while the widget’s builds keep calling it, and stops when a build no longer
does. Neither disposes the model; the State that created it does.
Value notifiers
Section titled “Value notifiers”ValueNotifier<T> is a Notifier with a single value. Assigning a different
value notifies for you, so it suits a model that would have one field:
class CartValueView extends StatelessWidget { const CartValueView({super.key, required this.count});
final ValueNotifier<int> count;
@override Widget build(BuildContext context) { final items = context.listen(count).value; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Items: $items'), Button(text: 'Add item', onPressed: () => count.value++), ], ); }}Who disposes what
Section titled “Who disposes what”A model is disposed by the code that created it:
| Created by | Disposed by |
|---|---|
A State field | That state’s dispose method |
Scope.create | The scope, when it leaves the tree. Pass dispose: for values that are not notifiers |
| Code outside the tree | That code, once nothing uses the model |
Scope(value), context.scope, ScopeBuilder, context.listen, and
NotifierBuilder only borrow a model, and their subscriptions end
automatically when the widget leaves the tree.
Next steps
Section titled “Next steps”- Widgets & state covers widget identity, lifecycle, and context.
- Loading data covers futures, streams, refresh, and stale results.
- Testing shows how to exercise state and user actions.
- The Shared state showcase puts local, scoped, and app-owned state in one app.