Like many other Flutter devs, over the past year I have been on a quest to create state management library that feels right to me. However, rather than trying to create a solution that is “better” in some abstract sense, I have been trying to create something that’s more like Flutter. What do I mean by this? For me, this has three essential components:
- It should be extremely easy to learn and use with very fast iteration cycles that continue to be fast as your app scales.
- It should not sacrifice power and flexibility for ease of use for newcomers. While it should be easy to learn, you should be expected to understand the basics of how it works under the hood to use it properly. No secret magic involved!
- It should feel native to the Flutter framework. I would like a solution that could have been written by the Flutter team themselves and you wouldn’t be able to tell the difference.
After quite a few iterations and full rewrites, I’ve come up with a state management pattern and library that implements the pattern that I think at least come very close to fulfilling all three criteria.
Observable State Trees
So what’s an Observable State Tree? An object that implements the observable pattern is simply a mutable object that can notify listeners when some part of it changes. So an Observable State Tree is a single tree of observable objects that maps onto your widget tree. Each node in the state tree has all the data and state needed for its corresponding widget to render and will notify the widget to rebuild upon changes. It also has a complete API to handle user interaction from that widget subtree.
Why Use the Observable State Tree Pattern?
The OST pattern has a number of benefits:
- All business logic is abstracted out of the widget tree and widgets are provided a minimal API for data, UI state and for responding to user interaction. Widgets only exist to display your UI and handle interaction.
- No newfangled mechanisms for providing state. You can simply use InheritedWidgets.
- Because the OST is strictly mapped to widget subtrees, there should be no more confusion about which inherited widget is or is not above your current build context. You should only depend on one inherited widget per widget which is the nearest inherited widget ancestor.
- Since your entire app state exists in a single tree structure, it is much easier to understand your app state and, importantly, the interdependencies between components of your state. You can understand your app state independently from how your widget tree is built. Just like the widget tree, the OST has one way data flow.
- The object oriented nature of observables make them easy to work with. Rather than dispatching some action with a complex reducer, you can simply change the value of a field of a state object or call a member function and your widgets will rebuild automatically.
- You can perform integration tests on your app state independently of your widget tree.
Show Me Some Code!
To implement the OST pattern we need 2 things. First, we need a type of observable that is composable. For this I’ve created a library called change_emitter. A ChangeEmitter is like a ChangeNotifier from the Flutter framework except:
- Instead of holding onto a list of callbacks, it exposes a stream of Change objects that are broadcast upon changes. This allows us to provide extra information about a particular change (like if a particular change should trigger the ChangeEmitter’s parent to also emit a change). Another benefit is that Dart streams are fantastic to work with, right out of the box!
- ChangeEmitters are composable and each holds onto a reference to its parent (in order to depend on state up the tree). Provided in the change_emitter library is the class EmitterConainer which can be extended to compose ChangeEmitters much like StatelessWidget or StatefulWidget can be extended to compose other widgets.
Second, we need an easy way of providing our state objects to their corresponding widget subtrees. The change_emitter library comes with a reimplementation of the provider package with a minimalistic API that is custom built to handle OST’s. The idea is that nothing particularly fancy should be happening in your widget tree. All complex business logic should be handled purely in the OST. All the Providers need to do is provide the next child in the OST in the appropriate place in your widget tree.