Skip to content

Instantly share code, notes, and snippets.

@rrousselGit
Created June 14, 2019 09:01
Show Gist options
  • Save rrousselGit/b85ff28f3ee9509a97171f5b944890b1 to your computer and use it in GitHub Desktop.
Save rrousselGit/b85ff28f3ee9509a97171f5b944890b1 to your computer and use it in GitHub Desktop.
import 'package:vanilla/reducer_provider.dart';
enum _Action {
increment,
decrement,
}
class MyStore extends ReducerStore<int, _Action> {
MyStore() : super(0);
@override
int reducer(int previousValue, _Action action) {
switch (action) {
case _Action.increment:
return previousValue = 1;
case _Action.decrement:
default:
return previousValue - 1;
}
}
void increment() => dispatch(_Action.increment);
void decrement() => dispatch(_Action.decrement);
}
class Bar {}
class Initial implements Bar {}
class Loading implements Bar {}
class Error implements Bar {
Error(this.err);
final Object err;
}
class Loaded implements Bar {
Loaded(this.value);
final int value;
}
class Foo extends Store<Bar> {
Foo() : super(Initial());
Future<void> fetch() async {
setState(Loading());
try {
final result = await Future<void>.delayed(Duration(seconds: 1));
setState(Loaded(42));
} catch (err) {
setState(Error(err));
}
}
}
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart';
abstract class SagaStore<Value, Action> extends Store<Value> {
SagaStore(Value initialValue) : super(initialValue) {
_subscription =
_behaviorSubject.distinct().flatMap((s) => s).listen(setState);
}
StreamSubscription<Value> _subscription;
final BehaviorSubject<Stream<Value>> _behaviorSubject = BehaviorSubject();
@protected
void dispatch(Action action) =>
_behaviorSubject.add(reducer(_notifier.value, action));
Stream<Value> reducer(Value previousValue, Action action);
@override
void dispose() {
_subscription.cancel();
_behaviorSubject.close();
super.dispose();
}
}
abstract class ReducerStore<Value, Action> extends Store<Value> {
ReducerStore(Value initialValue) : super(initialValue);
@protected
void dispatch(Action action) {
setState(reducer(value, action));
}
Value reducer(Value previousValue, Action action);
}
/// A base class for custom models.
///
/// As opposed to [ChangeNotifier], [Store] voluntarily does not implement
/// [Listenable], and instead implement its listening system through [valueListenable].
///
/// This allows a separation between the value and the logic to update that value.
///
/// That separation allows multiple things that [ChangeNotifier] cannot do:
///
/// - Make the value read-only, by making the [Store] subclass private,
/// but the class used for [value] public.
/// - It allows widgets to listen only to [Store] without listening to the
/// value updates. A typical example is an increment button.
/// - It is slightly safer, as we cannot forget update [value] without
/// notifying listeners.
///
/// See also:
///
/// * [StoreProvider], which listens to [Store] and expose both the value
/// and the store instance independently.
abstract class Store<Value> {
/// Initializes [value] for subclasses.
Store(Value initialValue) : _notifier = ValueNotifier<Value>(initialValue);
final ValueNotifier<Value> _notifier;
/// The current value.
///
/// External classes should not read this value directly.
///
/// Instead they should obtain it through `Provider.of<Value>`
/// where `Value` is the generic parameter specified to `Store<Value>`.
@protected
Value get value => _notifier.value;
ValueListenable<Value> get valueListenable => _notifier;
/// Synchronously updates [value] and notify listeners.
@protected
void setState(Value value) => _notifier.value = value;
@mustCallSuper
@protected
void dispose() {
_notifier.dispose();
}
}
/// A provider that exposes a [Store].
///
/// [StoreProvider] will expose both [Store] and [Store.value] independently
/// to its dependents.
///
/// For the store:
///
/// ```dart
/// class MyStore extends Store<int> {
/// MyStore(): super(0);
/// }
/// ```
///
/// it is possible to obtain the `MyStore` instance through:
///
/// ```dart
/// Provider.of<MyStore>(context);
/// ```
///
/// and the current value through:
///
/// ```dart
/// Provider.of<int>(context);
/// ```
class StoreProvider<Value, Controller extends Store<Value>>
extends ValueDelegateWidget<Controller>
implements SingleChildCloneableWidget {
/// Allows to specify parameters to [StoreProvider].
StoreProvider({
Key key,
@required ValueBuilder<Controller> builder,
Disposer<Controller> dispose,
Widget child,
}) : this._(
key: key,
delegate: BuilderStateDelegate<Controller>(builder, dispose: dispose),
updateShouldNotify: null,
child: child,
);
/// Allows to specify parameters to [StoreProvider].
StoreProvider.value({
Key key,
@required Controller value,
UpdateShouldNotify<Controller> updateShouldNotify,
Widget child,
}) : this._(
key: key,
delegate: SingleValueDelegate<Controller>(value),
updateShouldNotify: updateShouldNotify,
child: child,
);
StoreProvider._({
Key key,
@required ValueStateDelegate<Controller> delegate,
this.updateShouldNotify,
this.child,
}) : super(key: key, delegate: delegate);
/// User-provided custom logic for [InheritedWidget.updateShouldNotify].
final UpdateShouldNotify<Controller> updateShouldNotify;
@override
StoreProvider<Value, Controller> cloneWithChild(Widget child) {
return StoreProvider._(
key: key,
delegate: delegate,
updateShouldNotify: updateShouldNotify,
child: child,
);
}
/// The widget that is below the current [StoreProvider] widget in the
/// tree.
///
/// {@macro flutter.widgets.child}
final Widget child;
@override
Widget build(BuildContext context) {
assert(() {
Provider.debugCheckInvalidValueType?.call<Controller>(delegate.value);
return true;
}());
return InheritedProvider<Controller>(
value: delegate.value,
updateShouldNotify: updateShouldNotify,
child: ValueListenableProvider<Value>.value(
value: delegate.value.valueListenable,
child: child,
),
);
}
}
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:vanilla/example.dart';
import 'package:vanilla/reducer_provider.dart';
void main() {
group('Reducer test', () {
testWidgets('description', (tester) async {
final store = MyStore();
final key = GlobalKey();
await tester.pumpWidget(StoreProvider<int, MyStore>.value(
value: store,
child: Container(key: key),
));
expect(Provider.of<MyStore>(key.currentContext), equals(store));
expect(Provider.of<int>(key.currentContext), equals(0));
store.increment();
expect(Provider.of<int>(key.currentContext), equals(0));
await tester.pump();
expect(Provider.of<int>(key.currentContext), equals(1));
});
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment