Upstate: Behind the Scenes

Jonathan Aird
6 min readApr 28, 2020

Flutter state management should be simple!

Note: In an earlier article, I outlined the concept for this library using the observable dart package. It turns out the observable library hasn’t been maintained in years and was unsuitable for this use case. Upstate doesn’t rely on any packages and everything is built from scratch.

In my last article, I introduced Upstate, a state management library that aims to be much, much simpler than other solutions and yet powerful and extensible. For the sake of transparency, let’s take a look at how it works under the hood. If you want to rely on a state management library for a real app, you want to know that nothing crazy is going on behind the scenes and that it’s not likely to break easily. You might expect a lot of hokey code to achieve such a simple API but you’d be wrong! The way Upstate works is very simple and most of the magic is actually being done by Dart and the Flutter framework.

Basic Structure

Upstate takes a lot of inspiration from the way Flutter works, even behind the scenes. Let’s take a look at how your Flutter code gets turned into pixels on your screen (simplified):

Your Flutter code creates a widget tree and this widget tree communicates with the Flutter rendering engine to paint pixels onto the screen. When a user interacts with your app, your widget tree is modified based on the code that you’ve predefined for it. This provides a layer of abstraction separating you from platform specific details. You can extend Flutter with platform specific functionality using plugins (such as hardware access or OS permissions).

In Upstate, we have exactly the same pattern except for data objects instead of UI components:

First our deep map/list tree is converted to a state tree based on the options provided to it. Just like platform specific plugins, you can provide Upstate with a custom converter function with your own data structures. When you use the StateObject API, it mutates the state based on the rules and options that you’ve set for it. This provides a layer of abstraction between your code and the raw data in your state.

Just like everything in the widget tree is a widget, everything in the state tree is a state element!

State Elements

What are state elements?! StateElement is a very simple abstract class at that forms the base of this library. There are three main functions state elements have to perform. First and foremost it keep a reference to its parent in the state tree:

final StateElement parent;

State elements use parents to distribute state object options down the state tree. It also has some extra functionality we will see below.

The most essential functionality is provided by a broadcast stream:

final StreamController<StateElementNotification> _notifications =
StreamController.broadcast();

This stream controller is used to notify widgets that they need to rebuild because our state has changed. But to subscribe to notifications, we need to expose its stream:

Stream<StateElementNotification> get notifications => 
_notifications.stream;

So what if our state changed? How do we notify listeners?

void notifyChange() {
if (removedFromStateTree) {
throw ('State elements that have been removed from the
state tree can\'t be modified');
} else {
_notifications.add(StateElementNotification.changed);
if (notifyParent && !isRoot) {
parent.notifyChange();
}
}
}

Here we see some niceties of Upstate. Firstly if you try to modify a state element that has been removed from the state tree, it will automatically throw an error. Second, while we are using streams, this is not BLoC. The stream is not passing new state values. The only thing our stream will do is tell a stateful widget to update. Since the stateful widget already has a reference to the state element and its stored value, it just rebuilds.

Third, we can see that whenever a state element changes, it notifies its parent that it also changed (unless you turn off this setting) and notifications bubble up the state tree. That means you can subscribe to any part of the state tree, and you will get notified if descendants change. You could even subscribe to the root StateObject to get notified and rebuild a widget whenever any part of your state changes! Obviously you shouldn’t do that but you can!

Lifting State Up

Most of the heavy lifting is done in our state object. It creates the state tree and manages all of its state elements based on the rules you provide. All we have to do to provide state to widgets is “lift it up” in our widget tree. Let’s look at StateWidget:

class StateWidget extends InheritedWidget {
final Widget child;
final StateObject state;
StateWidget({@required this.state, @required this.child, Key key})
: super(key: key);
@override
bool updateShouldNotify(StateWidget oldWidget) {
if(oldWidget.state != state){
oldWidget.state.unmount();
return true;
} else{
return false;
}
}
}

That’s all there is! Note that while StateWidget is an inherited widget, it will only cause dependent widgets to rebuild if the entire state object is replaced with a new one, not when your state changes. If it is replaced, the old state object is unmounted and all state elements in the old state object are notified that they have been removed from the state tree. If you try to access or change any values of in the old state accidentally, Upstate will throw an error. However, this should not ever happen if used the recommended way.

Getting State

Getting your state object is very simple and done in StateObject.of:

static StateObject of<T extends StateWidget>(BuildContext context) 
=> context.dependOnInheritedWidgetOfExactType<T>()?.state;

The wonderful thing about this is that it’s O(1) no matter how deep you are in the widget tree so you can get state as liberally as you want! By contrast, findWidgetOfExactType is O(n) with depth. You can also subclass StateWidget to have multiple state widgets in your widget tree and get state from a particular one.

Updating Widgets

Let’s take a look at the code that actually updates widgets. At the deepest level, we have the subscribeTo method in StateObject:

StreamSubscription subscribeTo(StatePath path, 
VoidCallback callback) {

var element = getElementFromPath(path);
return element.notifications.listen((event) {
callback();
});
}

We’re just getting a state element from a path, listening to notifications, returning a StreamSubscription and calling a callback function on a state change. You can use this method but Upstate comes with a mixin for statefulwidget states that makes the process a bit nicer.

mixin StateConsumerMixin<T extends StatefulWidget> on State<T> {
List<StreamSubscription> subscriptions = [];

subscribeToPaths(List<StatePath> paths, StateObject state) {
for (var path in paths) {
subscriptions.add(
state.subscribeTo(
path,
_setStateSubscriptionCallback)
);
}
}
_setStateSubscriptionCallback() {
setState(() {});
}
cancelSubscriptions() {
for (var sub in subscriptions) {
sub.cancel();
}
}
}

Our mixin just allows us to subscribe to multiple paths or cancel all subscriptions in one go and provides the callback. All the callback does is call setState!

Using StateBuilder, which you saw in my introductory article, just provides a wrapper for a stateful widget.

Conclusion

Hopefully after reading this article, you can see that there’s nothing crazy happening to update your widgets upon a state change. Everything boils down to a notification stream, dependOnInheritedWidget and setState. Most of the magic is just being handled by Flutter! There’s a lesson here that I learned from from using the Flutter framework: When you work hard to find the right abstractions, your code becomes very simple, robust, and can be used for use cases that you have yet to imagine. In my next article, I will provide clarity on the data structures used in the state tree and the conversion to state elements. Stay tuned!

Cheers and happy coding!

--

--