elias8 / state_notifier

ValueNotifier, but outside Flutter and with some extra perks

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

pub package Welcome to state_notifier~

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 between state_notifier and Flutter.
    It adds things like ChangeNotifierProvider from provider, but compatible with state_notifier.

Motivation

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 & notifyListeners 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 services
  • update, 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 to state, to match the class name
  • StateNotifier is abstract
  • state is @protected
  • The listener passed to addListener receives the current state, and is called synchronously on addition.
  • addListener and removeListener are fused in a single addListener 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 to State.

About

ValueNotifier, but outside Flutter and with some extra perks

License:MIT License


Languages

Language:Dart 99.3%Language:Shell 0.7%