To Create Your Own State Management.

 In case you want to create your own state management


So you want to create your own state management package to get rich and famous.


Of course, it shouldn't have any boilerplate code to automatically rebuild widgets, even stateless ones, and thus be better and easier to use than any single existing widget.


Let's take a look at how to do state management without a package: We use a `StatefulWidget` and add the state as fields to its `State` class:


    class C0 extends StatefulWidget {

      const C0({super.key});


      @override

      State<C0> createState() => _C0State();

    }


    class _C0State extends State<C0> {

      var _count = 0;


      @override

      Widget build(BuildContext context) {

        return TextButton(

          onPressed: () => setState(() => _count++),

          child: Text('$_count'),

        );

      }

    }


We call this horrible, of course, because of all that boilerplate needed, i.e. the extra class definition, and because of an explicit call to `setState'.


Let's improve this by using a `ChangeNotifier`:


    class Counter extends ChangeNotifier {

      var _count = 0;


      int get count => _count;


      void increment() {

        _count++;

        notifyListeners();

      }

    }


And let's not worry about how the widget gets a reference to an instance of this class and use a global variable:


    final c1 = Counter();


Then, a `ListenableBuilder` is here to help us:


    class C1 extends StatelessWidget {

      const C1({super.key});


      @override

      Widget build(BuildContext context) {

        return ListenableBuilder(

          listenable: c1,

          builder: (context, _) {

            return TextButton(

              onPressed: c1.increment,

              child: Text('${c1.count}'),

            );

          },

        );

      }

    }


This is even worse, you might say, even if we now correctly separate the business logic (the counting) from the presentation (the UI).


In cases as simple as this, we could also use a `ValueNotifier` without a subclass, but the trade-off is that we are now once again mixing the logic and presentation.


    final c2 = ValueNotifier(0);


So let's quickly create a subclass:


    class Counter2 extends ValueNotifier<int> {

      Counter2() : super(0);

    

      void increment() => value++;

    }


    final c2 = Counter2();


We'll now use a `ValueListenableBuilder`:


    class C2 extends StatelessWidget {

      const C2({super.key});


      @override

      Widget build(BuildContext context) {

        return ValueListenableBuilder(

          valueListenable: c2,

          builder: (context, count, _) {

            return TextButton(

              onPressed: () => c2.value++,

              child: Text('$count'),

            );

          },

        );

      }

    }


How, as we have a base-line to compare our framework with, we'd of course like to write it this way and create an implicit dependency to our `Listenable` just by mentioning it:


    class C3 extends StatelessWidget {

      const C3({super.key});

    

      @override

      Widget build(BuildContext context) {

        return TextButton(

          onPressed: c2.increment,

          child: Text('${context.watch(c2)}'),

        );

      }

    }


Note, that I could have expressed it also as `c2.watch(context)`. I could have even written `watch(context, c2)` and use a global function.


    T watch<T>(BuildContext context, ValueNotifier<T> notifier) {

      notifier.addListener(() => (context as Element).markNeedsBuild();

      return notifier.value;

    }


At this point, we could call it a day and ship this, ignoring the fact that this code obviously has a memory leak, because it does its magic and triggers rebuilds on changes to the notifier, saving five lines of code.


Removing the listener when the `BuildContext` is unmounted is a bit tricky, because we don't want to create our own widget subclass (which could use a special `Element` subclass to help with listening). 


Enter `WeakReference` and `Finalizer` to the rescue!


But first we need an `Id` class that uniquely identifies a combination of context and notifier without keeping the context alive. I also use this class to notify the `Element` of the context that it needs to rebuild itself.


    class Id {

      Id(BuildContext c, this.l) : r = WeakReference(c as Element);

      final WeakReference<Element> r;

      final Listenable l;


      @override

      bool operator ==(other) => identical(this, other) || other is Id && l == other.l && r.target == other.r.target;


      @override

      int get hashCode => Object.hash(l, r.target);


      void markNeedsBuild() {

        if (r.target?.mounted ?? false) r.target?.markNeedsBuild();

      }

    }


We can now use `Id`s to remember if we have already subscribed.


    final _ids = <Id>{};


Second, we need a finalizer that removes all listeners of a notifier within that context when it dies:


    final _fin = Finalizer<VoidCallback>((l) => l());


Then, we can do the magic:


    T watch<T>(BuildContext context, ValueNotifier<T> notifier) {

      final id = Id(context, notifier);

      if (_ids.add(id)) {

        void listener() => id.markNeedsBuild();

        id.l.addListener(listener);

        _fin.attach(context, () {

          id.l.removeListener(listener);

          _ids.remove(id);

        });

      }

      return notifier.value;

    }


An easier approach is to create a special `WatchWidget` using a private `_WatchElement` to keep track of listenables. Notice that this needs to be a bit more clever in case of `update` when a new widget might register new listenables. I leave this to the reader.


    abstract class WatchWidget extends StatelessWidget {

      const WatchWidget({super.key});


      @override

      StatelessElement createElement() => _WatchElement(this);

    }


    class _WatchElement extends StatelessElement {

      _WatchElement(super.widget);


      final _l = <Listenable>{};


      T watch<T>(ValueNotifier<T> notifier) {

        if (_l.add(notifier)) {

          notifier.addListener(markNeedsBuild);

        }

        return notifier.value;

      }


      @override

      void unmount() {

        for (final l in _l) {

          l.removeListener(markNeedsBuild);

        }

        super.unmount();

      }

    }


    extension WatchWidgetExtension on BuildContext {

      T watch<T>(ValueNotifier<T> notifier) => (this as _WatchElement).watch(notifier);

    }


Ship it!


Now all that remains is to decide whether the ability to use `context.watch(notifier)` is actually worth the effort. Maybe if we add automatically disposable resources. That would be just a couple of additional lines in `_WatchElement`. Or if we support looking up values in the widget hierarchy and/or adding general dependency injection.


Disclaimer: This is not meant for production use and only a learning excercise.

Comments

Popular Posts