Provider + ChangeNotifier will do the same.
Flutter's State Manager for Reactive Apps in a Centralized and Predictable container.
- π§° State changes are handled using encapsulation on plain-old objects.
- βοΈ You can freely test your state and its changes, like pure Dart; because IT IS pure Dart.
- π― Notify only the specific
State
s that are observing the triggered change, NOT the entire Widget tree. - π§ No Streams or new Widgets, it is purely
State
andsetState()
; like Vanilla, but in a Centralized and Predictable state container. Your Container!
It's a sweet, sweet sugar on top of the battle-tested Observer pattern. Your state is stored in a list of Observable
s of a given subject (see Change
below) and it's setState()
is called only when your Model explicit tells to notify about that change.
- It isn't BLoC, but you still can maintain, test and visualize your Business Logic away from the UI.
- It isn't Flux (Redux/Rx/Stream), but you still can control data in an unidirectional flow.
The fact is: state is hard! Probably the hardest thing (after naming, of course). This is why things like BLoC, Flux/Redux and ScopedModel appears; to help you solve that.
And that is why observable_state is here too.
Talk is cheap. Show me the code β Torvalds, Linus
dependencies:
observable_state: ^0.2.0
class MyState {
int counter;
}
From a simple model like that, to add observable_state super-powers you just need to:
class MyState extends Observable<Changes> {
int counter;
}
Then make sure you declare what changes to your state are:
enum Changes {
increment,
}
Even better, remember that changes are handled by encapsulation:
enum Changes {
increment,
}
class MyState extends Observable<Changes> {
int _counter;
int get counter => _counter;
void increment() {
_counter++;
}
}
Observable state borrows the same Flutter API for local state (setState()
) with a slight difference: the notify
argument:
enum Changes {
increment,
}
class MyState extends Observable<Changes> {
int _counter;
int get counter => _counter;
void increment() {
setState(
() => _counter++,
notify: Changes.increment,
);
}
}
That is it. Your Observable
is notifying about MyState
changes.
Here comes the StateObserver
.
StatelessWidget
s are state-less as the name suggests. We are not going to mess with them adding Streams or whatever. Actually, even the StatefulWidget
remains the same. We are going to super-power the State
only!
class MyStatefulWidget extends StatefulWidget {
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends StateObserver<MyStatefulWidget, MyState, Changes> {
@override
List<Changes> get changes => [Changes.increment];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Observable state #${state.counter}')),
body: Center(child: Text('Counter: ${state.counter}')),
floatingActionButton: Builder(
builder: (context) {
return FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
state.increment();
},
);
},
),
);
}
}
Note that we are extending StateObserver
instead of State
and we are given a list of changes we are interest in, like Changes.increment
.
StateObserver
already got the state
so we can get counter
and we can call increment()
on it as well.
Whenever increment()
is called, since it notifies about Changes.increment
, whoever Observer
(like a StateObserver
) is observing this change, it will automatically calls its inner setState()
method, then rebuilding it.
The last piece: ObservableProvider
.
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ObservableProvider(
state: MyState(),
child: MaterialApp(home: MyStatefulWidget()),
);
}
}
It's an InheritedWidget
responsible to provide our state from its context.
The example above works great, but because it is very simple. When using things like FutureBuilder
s and/or Animations. No longer will be a good idea to have your entire State UI rebuilt after a change. You don't need to rebuild the entire Scaffold
and its AppBar
just because of the body
.
That is why Observable state comes with 3 more constructions for a fine-grained control over what should be rebuilt on changes.
ObserverBuilder
bind()
&bindWidget()
observableStateOf()
Works like any other builder in Flutter (StreamBuilder
, FutureBuilder
, AnimatedBuilder
etc), each builder is specialized on a task and the ObserverBuilder
job is to provide the Observable state from the ObserverProvider
and listen for changes:
ObserverBuilder<MyState, Changes>(
changes: [Changes.increment],
builder: (context, state) => Text(state.counter.toString())
);
Now just this little piece of Text
will rebuild after Changes.increment
is notified.
They work almost like ObserverBuilder
, but they don't listen for changes. They just provide (or binds) the current state to a function (bind
) or a builder bindWidget
. This is useful when you need just the state and don't need to listen for changes.
bind()
is special, because it returns a void Function()
, this is useful to immediately pass to on*
listeners, like:
RaisedButton(
onPressed: bind<MyState>(context, (state) => state.increment())
child: const Icon(Icons.add),
);
AppBar(
title: bindWidget<MyState>(context, (context, state) => Text(state.title)),
);
This is a generalized construction to get the state from whatever place you are. It is a sugar over: ObservableProvider.of<MyState>(context).state
.
StreamBuilder(
stream: observableStateOf<MyState>(context).someStream,
);
That is it! You're ready to Rock! πΈ
Observable
has no dependency at all with any Flutter APIs, so you can unit test your state using plain-old test
package. And you can test your Widgets using a mock or a real state without any hassle.
Check out the example directory for a complete example with async-flows, services, dependency injection, Firebase integration and a lot more!
Feel free to, and I'd be glad if you, try it and leave some feedback. Just be aware that while on early stages, API may change.