SimpleAppState¶
SimpleAppState is the central owner of all application state. It is intentionally independent from Flutter widgets and the widget tree.
This page explains what SimpleAppState is responsible for and how it should be used.
What SimpleAppState owns¶
A SimpleAppState instance owns:
all application state values
slot definitions and their types
listeners and batch update coordination
It does not own:
widgets
UI-local objects (controllers, animations, focus nodes)
rendering lifecycle
This separation keeps state lifetime explicit and predictable.
Creating a SimpleAppState¶
In most applications, SimpleAppState is created once and lives for the entire lifetime of the app.
final appState = SimpleAppState();
It is usually defined as a global or top-level variable, often in ui/app_state.dart.
Slots define state, not fields¶
SimpleAppState does not expose state as public fields.
Instead, all state is accessed through StateSlot<T> objects:
final count = appState.slot<int>('count', initial: 0);
This design ensures:
state is explicitly declared
slot types are fixed on first access
state identity is stable and globally referenceable
Caster for typed collections¶
Sometimes you store typed collections like List<T> or Map<String, T> in a slot. To preserve the element types when cloning or restoring from a dictionary, you must provide a caster function.
List<T> example:
final namesSlot = appState.slot<List<String>>(
'names',
caster: (raw) => (raw as List).cast<String>(),
);
Map<String, T> example:
final complexSlot = appState.slot<Map<String, List<String>>>(
'complex',
caster: (raw) => (raw as Map<String, dynamic>).map(
(k, v) => MapEntry(k, (v as List).cast<String>()),
),
);
Notes:
Primitive types (String, int, double, bool) or CloneableFile do not require a caster.
Nested collections always need a caster to ensure type safety.
After cloning or fromDict/loadFromDict, the caster guarantees that runtime type errors are avoided.
Value semantics and safety¶
All values stored in SimpleAppState follow value semantics.
When you read a value:
final value = count.get();
The returned object is always a deep copy.
This guarantees:
accidental mutation cannot corrupt state
state transitions are explicit
undo / redo and persistence are reliable
State can only be changed via set or update.
Batch updates¶
SimpleAppState supports explicit batch updates:
appState.batch(() {
count.set(10);
// update other slots
});
During a batch:
state changes are applied immediately
widget rebuilds are deferred
each subscribed widget rebuilds only once
batching can be nested
Batching is a first-class concept and should be used whenever multiple slots are updated together.
Cloning and snapshots¶
SimpleAppState supports deep cloning of state data:
final snapshot = appState.clone();
A cloned SimpleAppState is a pure data snapshot.
Clones:
copy all state values deeply
do not copy slots or listeners
contain no widget or runtime bindings
This design is intentional.
A clone represents a detached, serializable snapshot that can be safely stored, compared, or restored later.
To restore a snapshot into a running application, use replaceDataFrom:
appState.replaceDataFrom(snapshot);
This replaces only the stored state values, while preserving:
slot definitions
widget subscriptions
runtime listeners
Typical use cases include:
undo / redo stacks
testing state transitions
persistence and restoration
deterministic equality checks
This separation between state data and runtime wiring keeps undo and restore logic explicit and predictable. For more information on these topics, see Advanced Topics.
What SimpleAppState is not¶
SimpleAppState is not:
a dependency injection container
a widget tree manager
a context-based lookup system
It intentionally avoids magic. If a widget rebuilds, you should always be able to answer:
Which slot caused this rebuild?