Skip to content

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 inRead it with
One widgetState, updated with setStateThe state’s own fields
Widgets below one ownerThe owner’s State, shared with Scope(value)context.scope<T>() or ScopeBuilder<T>
Several widgets that also change itA Notifier created by Scope.createcontext.scope<T>() or ScopeBuilder<T>
Widgets and code outside the treeA Notifier created outside the tree, shared with Scope(model) or passed to widgetscontext.scope<T>(), context.listen, or NotifierBuilder

Use a StatefulWidget when a value belongs to one part of the UI. Keep it in State and update it with setState:

local_counter.dart
You write · editable
class LocalCounter extends StatefulWidget {
const LocalCounter({super.key});
@override
State<LocalCounter> createState() => _LocalCounterState();
}
class _LocalCounterState extends State<LocalCounter> {
int count = 0;
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Count: $count'),
Button(text: 'Increment', onPressed: () => setState(() => count++)),
],
);
}
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

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.

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:

You write · editable
class _ProjectScreenState extends State<ProjectScreen> {
var project = const Project('Atlas');
void switchProject() => setState(() {
project = project.name == 'Atlas'
? const Project('Beacon')
: const Project('Atlas');
});
@override
Widget build(BuildContext context) => Scope(
project,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ProjectTitle(),
const ProjectPath(),
Button(text: 'Switch project', onPressed: switchProject),
],
),
);
}
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

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.

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:

You write · editable
class Shop extends StatelessWidget {
const Shop({super.key});
@override
Widget build(BuildContext context) =>
Scope.create(Cart.new, child: const ShopScreen());
}
class ShopScreen extends StatelessWidget {
const ShopScreen({super.key});
@override
Widget build(BuildContext context) => const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CartBadge(),
SizedBox(height: 1),
AddToCartButton(product: 'coffee'),
AddToCartButton(product: 'tea'),
],
);
}
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

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().

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.

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:

You write · editable
class CartView extends StatelessWidget {
const CartView({super.key, required this.cart});
final Cart cart;
@override
Widget build(BuildContext context) => NotifierBuilder(
notifier: cart,
builder: (context, cart) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Items: ${cart.itemCount}'),
Button(text: 'Add item', onPressed: cart.addItem),
],
),
);
}
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

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.

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++),
],
);
}
}

A model is disposed by the code that created it:

Created byDisposed by
A State fieldThat state’s dispose method
Scope.createThe scope, when it leaves the tree. Pass dispose: for values that are not notifiers
Code outside the treeThat 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.