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!


Comments
Post a Comment