State Management Patterns Across Vue and Flutter

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

  1. Shared ancestor required. Two branches without a common parent cannot share the same instance.
  2. Lifetime coupling. A ChangeNotifier is 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:

  1. Create a lightweight singleton Refresher that holds no data—only a VoidCallback.
  2. Each page registers its own ChangeNotifier’s notifyListeners with that singleton.
  3. 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.

Tags: state-management Pinia vue Flutter Provider

Posted on Tue, 09 Jun 2026 16:53:31 +0000 by blackwidow