This repository is a set of packages that reimplements ValueNotifier outside of Flutter.
It is spread across two packages:
state_notifier
, a pure Dart package containing the reimplementation of ValueNotifier.
It comes with extra utilities for combining our "ValueNotifier" with provider and to test it.flutter_notifier
, a binding betweenstate_notifier
and Flutter.
It adds things like ChangeNotifierProvider from provider, but compatible withstate_notifier
.
Extracting ValueNotifier outside of Flutter in a separate package has two purposes:
- It allows Dart packages with no dependency on Flutter to use these
classes.
This means that we can use them on AngularDart for example. - It allows solving some common problems with the original ChangeNotifier/ValueNotifier and/or their combination with provider.
For example, by using state_notifier
instead of the original ValueNotifier, then
you get:
- A significant simplification of the integration with provider
- Simplified testing/mocking
- Improved performances on
addListener
¬ifyListeners
equivalents. - Extra safety through small API changes
Integration with provider/service locators
StateNotifier is easily compatible with provider through an extra mixin: LocatorMixin
.
Consider a typical StateNotifier written like a ValueNotifier:
class Count {
Count(this.count);
final int count;
}
class Counter extends StateNotifier<Count> {
Counter(): super(Count(0));
void increment() {
state = Count(state.count + 1);
}
}
In this example, we may want to use Provider.of
/context.read
to connect our
Counter
with external services.
To do so, simply mix-in LocatorMixin
as such:
class Counter extends StateNotifier<Count> with LocatorMixin {
// unchanged
}
This then gives you access to:
read
, a function to obtain servicesupdate
, a new life-cycle that can be used to listen to changes on a service
We could use them to change our Counter
incrementation to save the counter in
a DB when incrementing the value:
class Counter extends StateNotifier<Count> with LocatorMixin {
Counter(): super(Count(0))
void increment() {
state = Count(state.count + 1);
read<LocalStorage>().writeInt('count', state.count);
}
}
Where Counter
and LocalStorage
are defined using provider
this way:
void main() {
runApp(
MultiProvider(
providers: [
Provider(create: (_) => LocalStorage()),
StateNotifierProvider<Counter, Count>(create: (_) => Counter()),
],
child: MyApp(),
),
);
}
Then, Counter
/Count
are consumed using your typical context.watch
/Consumer
/context.select
/...:
@override
Widget build(BuildContext context) {
int count = context.watch<Count>().count;
return Scaffold(
body: Text('$count'),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<Counter>().increment(),
child: Icon(Icons.add),
),
);
}
Testing
When using LocatorMixin
, you may want to mock a dependency for your tests.
Of course, we still don't want to depend on Flutter/provider to do such a thing.
Similarly, since state
is protected, tests need a simple way to read the state.
As such, LocatorMixin
also adds extra utilities to help you with this scenario:
myStateNotifier.debugMockDependency<MyDependency>(myDependency);
print(myStateNotifier.debugState);
myStateNotifier.debugUpdate();
As such, if we want to test our previous Counter
, we could mock LocalStorage
this way:
test('increment and saves to local storage', () {
final mockLocalStorage = MockLocalStorage();
final counter = Counter()
..debugMockDependency<LocalStorage>(mockLocalStorage);
expect(counter.debugState, 0);
counter.increment(); // works fine since we mocked the LocalStorage
expect(counter.debugState, 1);
// mockito stuff
verify(mockLocalStorage.writeInt('int', 1));
});
Note: LocatorMixin
only works on StateNotifier
, if you try to use it on other classes by with LocatorMixin
then it will not work.
Differences with ValueNotifier
This is not a one-to-one reimplementation of ValueNotifier. It has some differences:
- ValueNotifier is instead named StateNotifier (to avoid name clash)
ValueNotifier.value
is renamed tostate
, to match the class name- StateNotifier is abstract
state
is@protected
- The listener passed to
addListener
receives the currentstate
, and is called synchronously on addition. addListener
andremoveListener
are fused in a singleaddListener
function which returns a function to remove the listener.
This makes adding and removing listeners O(1) versus O(N) for ValueNotifier.- listeners cannot add extra listeners.
This makes notifying listeners O(N) versus O(N²) for ValueNotifier - offers a
mounted
boolean to know if the StateNotifier was disposed or not, similar toState
.