Flutter state_notifier
ValueNotifierをFlutterから引き剥がしたものが出発点らしい
2つのパッケージを内包している
state_notifier
Pure Dart PackageなValueNotifier
flutter_notifier
state_notifierとFlutterを繋ぐ役割
state_notifierと互換性のあるChangeNotifierProviderみたいなやつを提供してくれる
providerに入ってるやつと基本的に同じだけど、ちょっと違う感じかな?
ValueNotifierではなくstate_notifierを使うことで、以下のようなメリットがある
providerとの連携がシンプルになる
テスト/モックがシンプルになる
addListener/notifyListenersよりも高速
Providerやservice locatorとの連携
code:notifier.dart
class Counter extends StateNotifier<int> {
// int型の単一の状態を持つのがStateNotifier
Counter(): super(0);
void increment() {
//単一で持っている状態はstateでアクセスする
state++;
}
}
LocatorMixinを使うと、contextがなくてもStateNotifierにアクセスできる
code:counter.dart
class Counter extends StateNotifier<int> with LocatorMixin {
}
他のStateNotifierを呼び出す場合はこう
code:counter.dart
class Counter extends StateNotifier<int> {
Counter(): super(0);
void increment() {
state++;
read<LocalStorage>().writeInt('count', state);
}
}
testing
LocatorMixinを使っている場合は、テストではこれをモックしたいはず
stateは@protectedで実装されている
LocatorMixinはこういう場合でも、いい感じに使えるような機能がある
code:test.dart
myStateNotifier.debugMockDependency<MyDependency>(myDependency);
print(myStateNotifier.debugState);
myStateNotifier.debugUpdate();
さっき書いたCounterをテストしたければ、LocalStorageをモックすれば良い
code:counter_test.dart
test('incrementして、ローカルストレージに保存する', () {
final mockLocalStorage = MockLocalStorage();
//debugMockDependencyを呼び出して、あらかじめ用意したMockStateNotifierを渡す
final counter = Counter()..debugMockDependency<LocalStorage>(mockLocalStorage);
//テストではdebugStateを見ると良いっぽい
expect(counter.debugState, 0);
counter.increment();
expect(counter,debugState, 1);
// mockioの機能でverifyする
verify(mockLocalStorage.writeInt('int', 1));
});
StateNotifier classについて
仕組み自体はわかりやすくて、superは<T>をコンストラクタの引数に取る
内部にはgetterとsetterがある
@overrideしてもOK
code:state_notifier.dart
abstract class StateNotifier<T> {
T _state;
StateNotifier(this._state);
...
@protected
T get state {
assert(_debugIsMounted(), '');
return _state;
}
@protected
set state(T value) {
assert(_debugIsMounted(), '');
_state = value;
_controller?.add(value);
var didThrow = false;
for (final listenerEntry in _listeners) {
try {
listenerEntry.listener(value);
} catch (error, stackTrace) {
didThrow = true;
if (onError != null) {
onError!(error, stackTrace);
} else {
Zone.current.handleUncaughtError(error, stackTrace);
}
}
}
if (didThrow) {
throw Error();
}
}
///
/// Will not work in release mode.
///
/// This is useful for tests.
T get debugState {
late T result;
assert(() {
result = _state;
return true;
}(), '');
return result;
}
}
code:locator_mixin.dart
/// A mixin that adds service location capability to an object.
///
/// This makes the object aware of things like Provider.of or GetIt, without
/// actually depending on those.\
/// It also provides testing utilities to be able to mock dependencies.
///
/// In the context of Flutter + provider, adding that mixin to an object
/// makes it impossible to shared one instance across multiple "providers".
///
/// This mix-in does not do anything by itself.\
/// It is simply an interface for 3rd party libraries such as provider to implement
/// the logic.
///
/// See also:
///
/// - read, to read objects /// - update, a new life-cycle to synchronize this object with external objects. mixin LocatorMixin {
// ignore: prefer_function_declarations_over_variables
Locator _locator = <T>() => throw DependencyNotFoundException<T>();
/// A function that allows obtaining other objects.
///
/// It is typically equivalent to Provider.of(context, listen: false) when
/// using provider, but it could also be GetIt.get for example.
///
/// **DON'T** modify read manually.\ /// The only reason why read is not final is so that it can be /// initialized by providers from flutter_notifier.
///
@protected
Locator get read {
assert(_debugIsNotifierMounted(), '');
return _locator;
}
set read(Locator read) {
assert(_debugIsNotifierMounted(), '');
_locator = read;
}
bool _debugIsNotifierMounted() {
assert(() {
if (this is StateNotifier) {
final instance = this as StateNotifier;
assert(instance._debugIsMounted(), '');
}
return true;
}(), '');
return true;
}
/// Overrides read to mock its behavior in development. ///
/// This does nothing in release mode and is useful only for test purpose.
///
/// A typical usage would be:
///
/// `dart
/// class MyServiceMock extends Mock implements MyService {}
///
/// test('mock dependency', () {
/// final myStateNotifier = MyStateNotifier();
/// myStateNotifier.debugMockDependency<MyService>(MyServiceMock());
/// });
/// `
void debugMockDependency<Dependency>(Dependency value) {
assert(_debugIsNotifierMounted(), '');
assert(() {
final previousLocator = read;
read = <Target>() {
assert(_debugIsNotifierMounted(), '');
if (Dependency == Target) {
return value as Target;
}
return previousLocator<Target>();
};
return true;
}(), '');
}
/// A life-cycle that allows initializing the StateNotifier based on values ///
/// This method will be called once, right before update. ///
/// It is useful as constructors cannot access read. @protected
void initState() {}
/// A life-cycle that allows listening to updates on another object.
///
/// This is equivalent to what "ProxyProviders" do using provider, but
/// implemented with no dependency on Flutter.
///
/// The property read is not accessible while inside the body of update. /// Use the parameter passed to update instead, which will not just read the /// object but also watch for changes.
@protected
void update(Locator watch) {}
var _debugDidInitState = false;
///
/// Further calls will only call update. ///
/// While inside update, read will be disabled as it would be in production. void debugUpdate() {
assert(() {
if (!_debugDidInitState) {
_debugDidInitState = true;
initState();
}
final locator = read;
read = <T>() => throw StateError('Cannot use read inside update');
try {
update(locator);
} finally {
read = locator;
}
return true;
}(), '');
}
}