Upstate is now ChangeEmitter: A Flexible, Highly Composable Alternative to ChangeNotifier

Earlier this year, I released Upstate, a state management library that attempted to emulate the properties that make Flutter so great. Flutter manages to balance simplicity, developer velocity, ergonomics (and fun!) with a robust framework that can easily scale to large professional apps with highly customized UI patterns and interactions. The ability to transition from the rapid prototyping stage to a fully custom, scalable app without changing tools or workflows is something that makes Flutter really special. This property is especially great for beginning programmers who can quickly get their feet wet and gradually master more complicated concepts.
Upstate used a nested map with string keys to store state which should be familiar to React developers as it’s similar to React component state. This made state management approachable and easy to set up and you could see your state in code. It also had the neat feature of being able to listen for changes in a particular branch of your state rather than just individual elements.
However the approach was not particularly scalable and had a complicated and somewhat unintuitive process for converting a nested map into objects that could update widgets on changes. I realized that you could have the same level of readability and ease of use by simply defining the elements of your state in a class. ChangeNotifier also now uses the Provider package to provide and consume state (if it ain’t broke…).
It turns out that to get the same desirable properties as Flutter, all you have to do is follow the same design principles! ChangeEmitter is similar to ChangeNotifier from the Flutter framework except that it’s highly composable. Just like you can create your own custom widget by composing a bunch of widgets, you can create a ChangeEmitter from other ChangeEmitters. This lets you create highly nested state objects and update your UI and other elements of your state automatically.
Let’s look at a simple example (check out the full source code here):
For this example we’re simply going to display some text, be able to modify the text and the styling of the text. Specifically we’re going to be able to change the text’s color and whether the text is bold and italic. We want our text widget to rebuild on changes to any of these values and to be able to change the values individually.
Our primitive values will be stored in ValueEmitters which are analogous to ValueNotifier from the Flutter framework.
var bold = ValueEmitter(false);
var italic = ValueEmitter(false);
var color = ValueEmitter(Colors.black);
ChangeEmitters expose a stream of change objects that are broadcast whenever they change:
var streamSubscription = bold.changes.listen((change)=>print('changed'));
bold.value=true; //prints 'changed'streamSubscription.cancel();
// we need to dispose ChangeEmitters when we're done with them
bold.dispose();
To compose our values into a single unit we’ll extend a class called EmitterContainer:
class TextState extends EmitterContainer {
final bold = ValueEmitter(false);
final italic = ValueEmitter(false);
final color = ValueEmitter(Colors.black); @override
get children => [bold, italic, color];}
TextState is also a ChangeEmitter and will emit changes whenever one of its children changes:
var textState = TextState();
var sub = textState.changes.listen((_)=>print('a child changed'));textState.bold.value = true; //prints 'a child changed'sub.cancel();
textState.dispose(); //disposes all children
Since everything in our state is a ChangeEmitter, we can have our state react to other parts of our state!
class TextState extends EmitterContainer {
final bold = ValueEmitter(false);
final italic = ValueEmitter(false);
final color = ValueEmitter(Colors.black);
ValueEmitter<bool> redAndBold; TextState(){
redAndBold = ValueEmitter.reactive(
[bold, color], //will react to these ChangeEmitters
()=>bold.value && color.value == Colors.red //with this value
); @override
get children => [bold, italic, color, redAndBold];
//We only want these children to cause TextState to emit changes
@override
get emittingChildren => [bold, italic, color];}
For our final touch, we will use a TextEditingEmitter which lets us control and read from a TextField:
class TextState extends EmitterContainer {
final textInput = TextEditingEmitter(text: 'Some text');
final bold = ValueEmitter(false);
final italic = ValueEmitter(false);
final color = ValueEmitter(Colors.black);
ValueEmitter<bool> isRedAndBold;TextState(){
isRedAndBold = ValueEmitter.reactive(
[bold, color], //will react to these ChangeEmitters
()=>bold.value && color.value == Colors.red //with this value
);@override
get children => [bold, italic, color, redAndBold, textInput]; //textInput is also a EmitterContainer and we only
//want to update our UI on changes in the text value
@override
get emittingChildren => [bold, italic, color, textInput.text];}
To provide our state we’ll use the Provider package:
void main(){
runApp(ChangeEmitterProvider(
create: (_)=> TextState(),
child:MyApp()
));
}
To consume just part of our state, we’ll use a special selector just for ChangeEmitters:
class BoldToggle extends StatelessWidget { @override
build(context){
return ChangeEmitterSelector<TextState,ValueEmitter<bool>(
selector: (_, textState) => textState.bold,
builder: (_, bold, __) {
return Switch(
value: bold.value
onChange: (newValue)=> bold.value = newValue,
);
}
);
}
}
To connect our state to a TextField, TextEditingEmitter provides a controller for us to use. There’s no need to dispose it since it will be disposed when our state is disposed!
class MyTextField extends StatelessWidget {
@override
build(context){
var textInput =
Provider.of<TextState>(context, listen:false).textInput;
return TextField(
controller:textInput.controller
);
}
}
To display our text we’ll use a vanilla Consumer widget:
class DisplayText extends StatelessWidget { @override
build(context){
return Consumer<TextState>(
builder: (_, textState, __) {
return Text(
textState.textInput.text.value
style: TextStyle(
color: textState.color.value,
fontSize: 24,
fontWeight:
textState.bold.value ? FontWeight.bold
: FontWeight.normal,
fontStyle:
state.italic.value ? FontStyle.italic :
FontStyle.normal,
),
);
}
);
}
}
If you have any questions or feedback feel free to open an issue on Github!
Happy building!