state_watcher

A simple, yet powerful reactive state management solution for Flutter applications.

Guide

The global state of an application can be seen as a set of independents sources of truth and derived states, computed from them and other ones.
For example in the counter app, the whole application has only one single source of truth: the counter.

In state_watcher, such a state is declared by a Provided:

// We declare here a state which has an initial value of 0
// and it can be referenced through `refCounter`.
final refCounter = Provided((_) => 0);

The actual state is stored in something called a Store. For that, in Flutter, we can declare a new store, in the widget tree, with a StateStore widget:

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // The root store can be declared just above the MaterialApp
    // so that it can be accessed from anywhere in the application.
    return const StateStore(
      child: MaterialApp(
        home: MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}

Then in order to display the value of our counter, we need to get the store. We can do that in any widget, with a WatcherBuilder!

WatcherBuilder(
  builder: (BuildContext context, BuildStore store) {
    // Thanks to the WatcherBuilder, we get the store from the nearest StateStore ancestor.
    // With this store we can watch the state referenced by `refCounter`.
    // Whenever the state changes, the builder of the WatcherBuilder will be called again.
    final counter = store.watch(refCounter);
    return Text('$counter');
  },
),

Now we need to be able to update the actual state, to do that we still need a store.
We could create another WatcherBuilder and use the store to update the value, but it can be cumbersome to deal with builder widgets.
Instead we will create a dedicated widget extending WatcherStatelessWidget!
A WatcherStatelessWidget is like a StatelessWidget except it has a different signature for the build method, in which we can get the store:

class _IncrementButton extends WatcherStatelessWidget {
  const _IncrementButton();

  @override
  Widget build(BuildContext context, BuildStore store) {
    // As with WatcherBuilder we can get the store.
    return FloatingActionButton(
      tooltip: 'Increment',
      onPressed: () {
        // We can then use the update method to changes the UI.
        store.update(refCounter, (x) => x + 1);
      },
      child: const Icon(Icons.add),
    );
  }
}

We saw the bare minimum to create an application using state_watcher, but what if we want to create a derived state?
For example let's say we want another widget displaying whether the counter can be divided by 3.

Such a state is declared by a Computed:

final refDivisibleByThree = Computed((watch) {
  final counter = watch(refCounter);
  final divisibleByThree = (counter % 3) == 0;
  return divisibleByThree;
});

And we can watch it like a Provided:

class _DivisibleByThree extends WatcherStatelessWidget {
  const _DivisibleByThree();

  @override
  Widget build(BuildContext context, BuildStore store) {
    final divisibleByThree = store.watch(refDivisibleByThree);
    return Text('$divisibleByThree');
  }
}

By default, the _DivisibleByThree widget is only rebuild when the new computed value is different than the previous one. So when the counter goes from 7 to 8, the _DivisibleByThree widget is not rebuilt because divisibleByThree is false for both values.

DevTools

state_watcher has a DevTools extension allowing you to easily debug the state changes in your app and see which is the state responsible for a widget to rebuild.

devtools_extension