Created
June 14, 2019 09:01
-
-
Save rrousselGit/b85ff28f3ee9509a97171f5b944890b1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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, | |
), | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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