What Is Application State?
Application state is any data that lives only while the program is running—values that may never reach a database or disk but still need to be shared, updated, and kept consistent across screens or components.
From Props Drilling to Global Stores
The simplest way to move data is through the component tree:
- Downward flow: A parent hands values to its children via attributes or constructor parameters.
- Upward flow: The parent passes a callback; the child invokes it with new data.
Three or more levels deep, this becomes "prop drillling": intermediate widgets receive and forward values they never use themselves. Global stores eliminate that indirection.
UI Reaction to State Changes
State libraries rarely repaint pixels themselves; instead, they expose reactive data. When the data mutates, every widget that subscribed is rebuilt automatically.
Vue + Pinia
Pinia exposes reactive stores. Any component can import a store and read or write its fields. A single assignment triggers re-rendering wherever that field is referenced.
// store.ts
export const useCart = defineStore('cart', () => {
const items = ref<Product[]>([]);
const add = (p: Product) => items.value.push(p);
return { items, add };
});
// AnyComponent.vue
const cart = useCart();
cart.add(product); // triggers re-render everywhere
Flutter + Provider
Provider is Flutter’s first-party answer. You wrap a common ancestor with a ChangeNotifierProvider, inject a ChangeNotifier, and descendants obtain it via context.watch<T>.
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // schedules rebuilds
}
}
// Somewhere above both widgets
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
)
// Any descendant
final counter = context.watch<CounterModel>();
Text('${counter.count}');
Provider uses the observer pattern: widgets subscribe, the model notifies.
Limitations of Provider
- Shared ancestor required. Two branches without a common parent cannot share the same instance.
- Lifetime coupling. A
ChangeNotifieris disposed when the wrapping route is popped. Navigating back later yields a brand-new object, breaking continuity.
Trying to solve (2) with a singleton fails because Flutter disposes the object on route removal; any later access throws. Keeping separate models per route solves disposal but breaks cross-route updates—each model owns its own listener list.
A Quick-and-Dirty Workaround
Suppose two pages, A and B, must react to the same counter. One unsightly trick is:
- Create a lightweight singleton
Refresherthat holds no data—only aVoidCallback. - Each page registers its own
ChangeNotifier’snotifyListenerswith that singleton. - When page A increments the counter, it also calls
Refresher.refresh(), forcing B to rebuild even though its model did not change.
class Refresher {
static final Refresher _i = Refresher._();
Refresher._();
factory Refresher() => _i;
final List<VoidCallback> _callbacks = [];
void register(VoidCallback fn) => _callbacks.add(fn);
void refresh() => _callbacks.forEach((fn) => fn());
}
// inside each model
Refresher().register(notifyListeners);
This works but is fragile: the singleton outlives routes, and models still get disposed, so the callback list must be pruned careful. A more robust solution is to lift the state above the Navigator (e.g., with Provider at the MaterialApp level) or migrate to a global store such as Riverpod or Bloc that decouples lifecycle from routes.