├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── analysis_options.yaml ├── example ├── example.dart └── example.g.dart ├── lib └── flutter_built_redux.dart ├── pubspec.yaml ├── test └── unit │ ├── flutter_built_redux_test.dart │ ├── test_models.dart │ ├── test_models.g.dart │ └── test_widget.dart └── tool └── build.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .atom/ 2 | .idea/ 3 | 4 | # dart 5 | .packages 6 | .pub 7 | packages 8 | pubspec.lock 9 | .dart_tool/ 10 | 11 | # flutter 12 | build/ 13 | 14 | # docs 15 | dartdoc-viewer/ 16 | 17 | # coverage 18 | /coverage/ 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | sudo: false 4 | addons: 5 | apt: 6 | sources: 7 | - ubuntu-toolchain-r-test 8 | packages: 9 | - libstdc++6 10 | - fonts-droid 11 | before_script: 12 | - git clone https://github.com/flutter/flutter.git --depth 1 13 | - ./flutter/bin/flutter doctor 14 | script: 15 | - ./flutter/bin/flutter test --coverage --coverage-path=lcov.info 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) 18 | cache: 19 | directories: 20 | - $HOME/.pub-cache -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.6.0 2 | * Migrate to Dart 2 3 | * Bump dependencies to more recent versions 4 | 5 | ## 0.5.0 6 | 7 | * **Breaking changes**: 8 | * Renamed the typdef WidgetBuilder to StoreConnectionBuilder to avoid possible namespacing issues. 9 | 10 | ## 0.4.5 11 | 12 | * create explicit typedefs for funcs passed to StoreConnection, as the new Function syntax messes up the analyzer 13 | 14 | ## 0.4.4 15 | 16 | * add StoreConnection, which is an implementation of store connector that takes a connect & builder function as parameters. 17 | * add example file. 18 | 19 | ## 0.4.3 20 | 21 | * Move built_value dependency to dev_dependencies and bump to ^5.0.0 22 | 23 | ## 0.4.2 24 | 25 | * Use assert for type assertions rather than thowing exceptions 26 | 27 | ## 0.4.1 28 | 29 | * Add changelog 30 | 31 | ## 0.4.0 32 | 33 | * **Breaking changes**: 34 | 35 | * Remove state builder generic from StoreConnector 36 | 37 | * Made StoreConnectorState private 38 | 39 | * Perform check on storeSub in didChangeDependencies to return early if a subscription to the store has already been created. 40 | 41 | * Raise exceptions if the store found by inheritFromWidgetOfExactType has different generics than the StoreConnector 42 | 43 | * Add unit tests 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Marne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Pub](https://img.shields.io/pub/v/flutter_built_redux.svg)](https://pub.dartlang.org/packages/flutter_built_redux) 2 | [![codecov.io](http://codecov.io/github/davidmarne/flutter_built_redux/coverage.svg?branch=master)](http://codecov.io/github/davidmarne/flutter_built_redux?branch=master) 3 | 4 | # flutter_built_redux 5 | 6 | [built_redux] bindings for Flutter. 7 | 8 | By creating a Widget that extends StoreConnector you get automatic subscribing to your redux store, and you component will only call setState when the store triggers and the values you take from the store in connect change! 9 | 10 | ## Examples 11 | 12 | [counter example](example/example.dart) 13 | 14 | [todo_mvc], written by [Brian Egan] 15 | 16 | ## Why you may need flutter_built_redux 17 | 18 | For the same reason you would want to use redux with react. 19 | 20 | from the flutter tutorial: 21 | 22 | > In Flutter, change notifications flow “up” the widget hierarchy by way of callbacks, while current state flows “down” to the stateless widgets that do presentation. 23 | 24 | Following this pattern requires you to send any state or state mutator callbacks that are common between your widgets down from some common ancestor. 25 | 26 | With larger applications this is very tedious, leads to large widget constructors, and this pattern causes flutter to rerun the build function on all widgets between the ancestor that contains the state and the widget that actually cares about it. It also means your business logic and network requests live in your widget declarations. 27 | 28 | [built_redux] gives you a predicable state container that can live outside your widgets and perform logic in action middleware. 29 | 30 | flutter_built_redux lets a widget to subscribe to the pieces of the redux state tree that it cares about. It also lets lets widgets dispatch actions to mutate the redux state tree. This means widgets can access and mutate application state without the state and state mutator callbacks being passed down from its ancestors! 31 | 32 | [built_redux]: https://github.com/davidmarne/built_redux 33 | 34 | [todo_mvc]: https://gitlab.com/brianegan/flutter_architecture_samples/tree/master/example/built_redux 35 | 36 | [Brian Egan]: https://gitlab.com/brianegan 37 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: 3 | implicit-casts: false 4 | implicit-dynamic: false -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | library example; 2 | 3 | import 'package:built_redux/built_redux.dart'; 4 | import 'package:flutter_built_redux/flutter_built_redux.dart'; 5 | import 'package:flutter/material.dart' hide Builder; 6 | import 'package:built_value/built_value.dart'; 7 | 8 | part 'example.g.dart'; 9 | 10 | void main() { 11 | // create the store 12 | final store = new Store( 13 | reducerBuilder.build(), 14 | new Counter(), 15 | new CounterActions(), 16 | ); 17 | 18 | runApp(new ConnectionExample(store)); 19 | // or comment the line above and uncomment the line below 20 | // runApp(new ConnectorExample(store)); 21 | } 22 | 23 | /// an example using `StoreConnection` 24 | class ConnectionExample extends StatelessWidget { 25 | final Store store; 26 | 27 | ConnectionExample(this.store); 28 | 29 | @override 30 | Widget build(BuildContext context) => new MaterialApp( 31 | title: 'flutter_built_redux_test', 32 | home: new ReduxProvider( 33 | store: store, 34 | child: new StoreConnection( 35 | connect: (state) => state.count, 36 | builder: (BuildContext context, int count, CounterActions actions) { 37 | return new Scaffold( 38 | body: new Row( 39 | children: [ 40 | new RaisedButton( 41 | onPressed: actions.increment, 42 | child: new Text('Increment'), 43 | ), 44 | new Text('Count: $count'), 45 | ], 46 | ), 47 | ); 48 | }, 49 | ), 50 | ), 51 | ); 52 | } 53 | 54 | /// an example using a widget that implements `StoreConnector` 55 | class ConnectorExample extends StatelessWidget { 56 | final Store store; 57 | 58 | ConnectorExample(this.store); 59 | 60 | @override 61 | Widget build(BuildContext context) { 62 | return new MaterialApp( 63 | title: 'flutter_built_redux_test', 64 | home: new ReduxProvider( 65 | store: store, 66 | child: new CounterWidget(), 67 | ), 68 | ); 69 | } 70 | } 71 | 72 | /// [CounterWidget] impelments [StoreConnector] manually 73 | class CounterWidget extends StoreConnector { 74 | CounterWidget(); 75 | 76 | @override 77 | int connect(Counter state) => state.count; 78 | 79 | @override 80 | Widget build(BuildContext context, int count, CounterActions actions) => 81 | new Scaffold( 82 | body: new Row( 83 | children: [ 84 | new RaisedButton( 85 | onPressed: actions.increment, 86 | child: new Text('Increment'), 87 | ), 88 | new Text('Count: $count'), 89 | ], 90 | ), 91 | ); 92 | } 93 | 94 | // Built redux counter state, actions, and reducer 95 | 96 | ReducerBuilder reducerBuilder = 97 | new ReducerBuilder() 98 | ..add(CounterActionsNames.increment, (s, a, b) => b.count++); 99 | 100 | abstract class CounterActions extends ReduxActions { 101 | factory CounterActions() => new _$CounterActions(); 102 | CounterActions._(); 103 | 104 | ActionDispatcher get increment; 105 | } 106 | 107 | abstract class Counter implements Built { 108 | factory Counter() => new _$Counter._(count: 0); 109 | Counter._(); 110 | 111 | int get count; 112 | } 113 | -------------------------------------------------------------------------------- /example/example.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of example; 4 | 5 | // ************************************************************************** 6 | // BuiltReduxGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: avoid_classes_with_only_static_members 10 | // ignore_for_file: annotate_overrides 11 | 12 | class _$CounterActions extends CounterActions { 13 | factory _$CounterActions() => new _$CounterActions._(); 14 | _$CounterActions._() : super._(); 15 | 16 | final ActionDispatcher increment = 17 | new ActionDispatcher('CounterActions-increment'); 18 | 19 | @override 20 | void setDispatcher(Dispatcher dispatcher) { 21 | increment.setDispatcher(dispatcher); 22 | } 23 | } 24 | 25 | class CounterActionsNames { 26 | static final ActionName increment = 27 | new ActionName('CounterActions-increment'); 28 | } 29 | 30 | // ************************************************************************** 31 | // BuiltValueGenerator 32 | // ************************************************************************** 33 | 34 | // ignore_for_file: always_put_control_body_on_new_line 35 | // ignore_for_file: annotate_overrides 36 | // ignore_for_file: avoid_annotating_with_dynamic 37 | // ignore_for_file: avoid_catches_without_on_clauses 38 | // ignore_for_file: avoid_returning_this 39 | // ignore_for_file: lines_longer_than_80_chars 40 | // ignore_for_file: omit_local_variable_types 41 | // ignore_for_file: prefer_expression_function_bodies 42 | // ignore_for_file: sort_constructors_first 43 | // ignore_for_file: unnecessary_const 44 | // ignore_for_file: unnecessary_new 45 | 46 | class _$Counter extends Counter { 47 | @override 48 | final int count; 49 | 50 | factory _$Counter([void updates(CounterBuilder b)]) => 51 | (new CounterBuilder()..update(updates)).build(); 52 | 53 | _$Counter._({this.count}) : super._() { 54 | if (count == null) throw new BuiltValueNullFieldError('Counter', 'count'); 55 | } 56 | 57 | @override 58 | Counter rebuild(void updates(CounterBuilder b)) => 59 | (toBuilder()..update(updates)).build(); 60 | 61 | @override 62 | CounterBuilder toBuilder() => new CounterBuilder()..replace(this); 63 | 64 | @override 65 | bool operator ==(Object other) { 66 | if (identical(other, this)) return true; 67 | return other is Counter && count == other.count; 68 | } 69 | 70 | @override 71 | int get hashCode { 72 | return $jf($jc(0, count.hashCode)); 73 | } 74 | 75 | @override 76 | String toString() { 77 | return (newBuiltValueToStringHelper('Counter')..add('count', count)) 78 | .toString(); 79 | } 80 | } 81 | 82 | class CounterBuilder implements Builder { 83 | _$Counter _$v; 84 | 85 | int _count; 86 | int get count => _$this._count; 87 | set count(int count) => _$this._count = count; 88 | 89 | CounterBuilder(); 90 | 91 | CounterBuilder get _$this { 92 | if (_$v != null) { 93 | _count = _$v.count; 94 | _$v = null; 95 | } 96 | return this; 97 | } 98 | 99 | @override 100 | void replace(Counter other) { 101 | if (other == null) throw new ArgumentError.notNull('other'); 102 | _$v = other as _$Counter; 103 | } 104 | 105 | @override 106 | void update(void updates(CounterBuilder b)) { 107 | if (updates != null) updates(this); 108 | } 109 | 110 | @override 111 | _$Counter build() { 112 | final _$result = _$v ?? new _$Counter._(count: count); 113 | replace(_$result); 114 | return _$result; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/flutter_built_redux.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | import 'package:flutter/widgets.dart' hide Builder; 5 | import 'package:built_redux/built_redux.dart'; 6 | 7 | /// [Connect] maps state from the store to the local state that a give 8 | /// component cares about 9 | typedef LocalState Connect(StoreState state); 10 | 11 | /// [StoreConnectionBuilder] returns a widget given context, local state, and actions 12 | typedef Widget StoreConnectionBuilder( 13 | BuildContext context, LocalState state, Actions actions); 14 | 15 | /// [StoreConnection] is a widget that rebuilds when the redux store 16 | /// has triggered and the connect function yields a new result. It is an implementation 17 | /// of `StoreConnector` that takes a connect function and builder function as parameters 18 | /// so `StoreConnector` doesn't have to be implemented manually. 19 | /// 20 | /// [connect] takes the current state of the redux store and retuns an object that contains 21 | /// the subset of the redux state tree that this component cares about. 22 | /// It requires that you return a comparable type to ensure your props setState is only called when necessary. 23 | /// Primitive types, built values, and collections are recommended. 24 | /// The result of [connect] is what gets passed to the build function's second param 25 | /// 26 | /// [builder] is a function that takes a `BuildContext`, the `LocalState` returned from 27 | /// connect, and your `ReduxActions` class and returns a widget. 28 | /// 29 | /// [StoreState] is the generic type of your built_redux store's state object 30 | /// [Actions] is the generic tyoe of your built_redux store's actions contiainer 31 | /// [LocalState] is the state from your store that this widget needs to render. 32 | /// [LocalState] should be comparable. It is recommended to only use primitive or built types. 33 | class StoreConnection 34 | extends StoreConnector { 35 | final Connect _connect; 36 | final StoreConnectionBuilder _builder; 37 | 38 | StoreConnection({ 39 | @required LocalState connect(StoreState state), 40 | @required 41 | Widget builder(BuildContext context, LocalState state, Actions actions), 42 | Key key, 43 | }) 44 | : assert(connect != null, 'StoreConnection: connect must not be null'), 45 | assert(builder != null, 'StoreConnection: builder must not be null'), 46 | _connect = connect, 47 | _builder = builder, 48 | super(key: key); 49 | 50 | @protected 51 | LocalState connect(StoreState state) => _connect(state); 52 | 53 | @protected 54 | Widget build(BuildContext context, LocalState state, Actions actions) => 55 | _builder(context, state, actions); 56 | } 57 | 58 | /// [StoreConnector] is a widget that rebuilds when the redux store 59 | /// has triggered and the connect function yields a new result. 60 | /// [StoreState] is the generic type of your built_redux store's state object 61 | /// [Actions] is the generic tyoe of your built_redux store's actions contiainer 62 | /// [LocalState] is the state from your store that this widget needs to render. 63 | /// [LocalState] should be comparable. It is recommended to only use primitive or built types. 64 | abstract class StoreConnector extends StatefulWidget { 66 | StoreConnector({Key key}) : super(key: key); 67 | 68 | /// [connect] takes the current state of the redux store and retuns an object that contains 69 | /// the subset of the redux state tree that this component cares about. 70 | /// It requires that you return a comparable type to ensure your props setState is only called when necessary. 71 | /// Primitive types, built values, and collections are recommended. 72 | /// The result of [connect] is what gets passed to the build function's second param 73 | @protected 74 | LocalState connect(StoreState state); 75 | 76 | @override 77 | _StoreConnectorState createState() => 78 | new _StoreConnectorState(); 79 | 80 | @protected 81 | Widget build(BuildContext context, LocalState state, Actions actions); 82 | } 83 | 84 | class _StoreConnectorState 85 | extends State> { 86 | StreamSubscription> _storeSub; 87 | 88 | /// [LocalState] is an object that contains the subset of the redux state tree that this component 89 | /// cares about. 90 | LocalState _state; 91 | 92 | Store get _store { 93 | // get the store from the ReduxProvider ancestor 94 | final ReduxProvider reduxProvider = 95 | context.inheritFromWidgetOfExactType(ReduxProvider); 96 | 97 | // if it is not found raise an error 98 | assert(reduxProvider != null, 99 | 'Store was not found, make sure ReduxProvider is an ancestor of this component.'); 100 | 101 | assert(reduxProvider.store.state is StoreState, 102 | 'Store found was not the correct type, make sure StoreConnector\'s generic for StoreState matches the state type of your built_redux store.'); 103 | 104 | assert(reduxProvider.store.actions is Actions, 105 | 'Store found was not the correct type, make sure StoreConnector\'s generic for Actions matches the actions type of your built_redux store.'); 106 | 107 | return reduxProvider.store; 108 | } 109 | 110 | /// sets up a subscription to the store 111 | @override 112 | @mustCallSuper 113 | void didChangeDependencies() { 114 | super.didChangeDependencies(); 115 | 116 | // if the store has already been subscribed to return early. didChangeDependencies 117 | // will be called every time the dependencies of the widget change, but we only 118 | // want to subscribe to the store the first time it is called. Subscriptions are setup 119 | // in didChangeDependencies, rather than initState, because inheritFromWidgetOfExactType 120 | // cannot be called before initState completes. 121 | // See https://github.com/flutter/flutter/blob/0.0.20/packages/flutter/lib/src/widgets/framework.dart#L3721 122 | if (_storeSub != null) return; 123 | 124 | // set the initial state 125 | _state = widget.connect(_store.state as StoreState); 126 | 127 | // listen to changes 128 | _storeSub = _store 129 | .substateStream((state) => widget.connect(state as StoreState)) 130 | .listen((change) { 131 | setState(() { 132 | _state = change.next; 133 | }); 134 | }); 135 | } 136 | 137 | /// Cancels the store subscription. 138 | @override 139 | @mustCallSuper 140 | void dispose() { 141 | _storeSub.cancel(); 142 | super.dispose(); 143 | } 144 | 145 | @override 146 | Widget build(BuildContext context) => 147 | widget.build(context, _state, _store.actions as Actions); 148 | } 149 | 150 | /// [ReduxProvider] provides access to the redux store to descendant widgets. 151 | /// [ReduxProvider] must be an ancesestor of a `StoreConnector`, otherwise the 152 | /// `StoreConnector` will throw during initialization. 153 | class ReduxProvider extends InheritedWidget { 154 | ReduxProvider({Key key, @required this.store, @required Widget child}) 155 | : super(key: key, child: child); 156 | 157 | /// [store] is a reference to the redux store 158 | final Store store; 159 | 160 | @override 161 | bool updateShouldNotify(ReduxProvider old) => store != old.store; 162 | } 163 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | # Important: Use "flutter packages get", not "pub get". 2 | name: flutter_built_redux 3 | author: David Marne 4 | version: 0.6.0 5 | description: Built_redux provider for Flutter 6 | homepage: https://github.com/davidmarne/flutter_built_redux 7 | dependencies: 8 | built_redux: ">=6.1.1 <8.0.0" 9 | flutter: 10 | sdk: flutter 11 | meta: ^1.0.3 12 | 13 | dev_dependencies: 14 | build_runner: ">=0.6.0 <0.11.0" 15 | built_value: ">=5.0.0 <7.0.0" 16 | built_value_generator: ">=5.0.0 <7.0.0" 17 | flutter_test: 18 | sdk: flutter 19 | #source_gen: ^0.7.0 20 | 21 | environment: 22 | sdk: ">=2.0.0 <3.0.0" 23 | -------------------------------------------------------------------------------- /test/unit/flutter_built_redux_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_redux/built_redux.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'test_models.dart'; 6 | import 'test_widget.dart'; 7 | 8 | void main() { 9 | Store store; 10 | 11 | setUp(() { 12 | store = createStore(); 13 | }); 14 | 15 | tearDown(() async { 16 | await store.dispose(); 17 | }); 18 | 19 | group('flutter_built_redux: ', () { 20 | group('StoreConnector: ', () { 21 | testWidgets('renders default state correctly', 22 | (WidgetTester tester) async { 23 | final providerWidget = new ProviderWidgetConnector(store); 24 | 25 | await tester.pumpWidget(providerWidget); 26 | 27 | CounterWidget counterWidget = tester.firstWidget( 28 | find.byKey(counterKey), 29 | ); 30 | 31 | Text incrementTextWidget = tester.firstWidget( 32 | find.byKey(incrementTextKey), 33 | ); 34 | 35 | expect(counterWidget.numBuilds, 1); 36 | expect(incrementTextWidget.data, 'Count: 0'); 37 | }); 38 | 39 | testWidgets('rerenders after increment', (WidgetTester tester) async { 40 | final widget = new ProviderWidgetConnector(store); 41 | 42 | await tester.pumpWidget(widget); 43 | 44 | CounterWidget counterWidget = tester.firstWidget( 45 | find.byKey(counterKey), 46 | ); 47 | 48 | Text incrementTextWidget = tester.firstWidget( 49 | find.byKey(incrementTextKey), 50 | ); 51 | 52 | expect(counterWidget.numBuilds, 1); 53 | expect(incrementTextWidget.data, 'Count: 0'); 54 | 55 | await tester.tap(find.byKey(incrementButtonKey)); 56 | await tester.pump(); 57 | 58 | counterWidget = tester.firstWidget( 59 | find.byKey(counterKey), 60 | ); 61 | 62 | incrementTextWidget = tester.firstWidget( 63 | find.byKey(incrementTextKey), 64 | ); 65 | 66 | expect(counterWidget.numBuilds, 2); 67 | expect(incrementTextWidget.data, 'Count: 1'); 68 | }); 69 | 70 | testWidgets('does not rerender after update to other counter', 71 | (WidgetTester tester) async { 72 | final widget = new ProviderWidgetConnector(store); 73 | 74 | await tester.pumpWidget(widget); 75 | 76 | CounterWidget counterWidget = tester.firstWidget( 77 | find.byKey(counterKey), 78 | ); 79 | 80 | Text incrementTextWidget = tester.firstWidget( 81 | find.byKey(incrementTextKey), 82 | ); 83 | 84 | expect(counterWidget.numBuilds, 1); 85 | expect(incrementTextWidget.data, 'Count: 0'); 86 | 87 | await tester.tap(find.byKey(incrementOtherButtonKey)); 88 | await tester.pump(); 89 | 90 | counterWidget = tester.firstWidget( 91 | find.byKey(counterKey), 92 | ); 93 | 94 | incrementTextWidget = tester.firstWidget( 95 | find.byKey(incrementTextKey), 96 | ); 97 | 98 | // pump should not cause a rebuild 99 | expect(counterWidget.numBuilds, 1); 100 | expect(incrementTextWidget.data, 'Count: 0'); 101 | }); 102 | }); 103 | 104 | group('StoreConnection: ', () { 105 | testWidgets('renders default state correctly', 106 | (WidgetTester tester) async { 107 | final providerWidget = new ProviderWidgetConnection(store); 108 | 109 | await tester.pumpWidget(providerWidget); 110 | 111 | Text incrementTextWidget = tester.firstWidget( 112 | find.byKey(incrementTextKey), 113 | ); 114 | 115 | expect(providerWidget.numBuilds, 1); 116 | expect(incrementTextWidget.data, 'Count: 0'); 117 | }); 118 | 119 | testWidgets('rerenders after increment', (WidgetTester tester) async { 120 | final providerWidget = new ProviderWidgetConnection(store); 121 | 122 | await tester.pumpWidget(providerWidget); 123 | 124 | Text incrementTextWidget = tester.firstWidget( 125 | find.byKey(incrementTextKey), 126 | ); 127 | 128 | expect(providerWidget.numBuilds, 1); 129 | expect(incrementTextWidget.data, 'Count: 0'); 130 | 131 | await tester.tap(find.byKey(incrementButtonKey)); 132 | await tester.pump(); 133 | 134 | incrementTextWidget = tester.firstWidget( 135 | find.byKey(incrementTextKey), 136 | ); 137 | 138 | expect(providerWidget.numBuilds, 2); 139 | expect(incrementTextWidget.data, 'Count: 1'); 140 | }); 141 | 142 | testWidgets('does not rerender after update to other counter', 143 | (WidgetTester tester) async { 144 | final providerWidget = new ProviderWidgetConnection(store); 145 | 146 | await tester.pumpWidget(providerWidget); 147 | 148 | Text incrementTextWidget = tester.firstWidget( 149 | find.byKey(incrementTextKey), 150 | ); 151 | 152 | expect(providerWidget.numBuilds, 1); 153 | expect(incrementTextWidget.data, 'Count: 0'); 154 | 155 | await tester.tap(find.byKey(incrementOtherButtonKey)); 156 | await tester.pump(); 157 | 158 | incrementTextWidget = tester.firstWidget( 159 | find.byKey(incrementTextKey), 160 | ); 161 | 162 | // pump should not cause a rebuild 163 | expect(providerWidget.numBuilds, 1); 164 | expect(incrementTextWidget.data, 'Count: 0'); 165 | }); 166 | }); 167 | }); 168 | } 169 | -------------------------------------------------------------------------------- /test/unit/test_models.dart: -------------------------------------------------------------------------------- 1 | library test_models; 2 | 3 | import 'package:built_value/built_value.dart'; 4 | import 'package:built_redux/built_redux.dart'; 5 | 6 | part 'test_models.g.dart'; 7 | 8 | Store createStore() => new Store( 9 | createReducer(), 10 | new Counter(), 11 | new CounterActions(), 12 | ); 13 | 14 | Reducer createReducer() => 15 | (new ReducerBuilder() 16 | ..add(CounterActionsNames.increment, (s, a, b) => b.count++) 17 | ..add(CounterActionsNames.incrementOther, (s, a, b) => b.other++)) 18 | .build(); 19 | 20 | abstract class CounterActions extends ReduxActions { 21 | factory CounterActions() => new _$CounterActions(); 22 | CounterActions._(); 23 | 24 | ActionDispatcher get increment; 25 | ActionDispatcher get incrementOther; 26 | } 27 | 28 | abstract class Counter implements Built { 29 | factory Counter() => new _$Counter._( 30 | count: 0, 31 | other: 0, 32 | ); 33 | Counter._(); 34 | 35 | int get count; 36 | int get other; 37 | } 38 | -------------------------------------------------------------------------------- /test/unit/test_models.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of test_models; 4 | 5 | // ************************************************************************** 6 | // BuiltReduxGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: avoid_classes_with_only_static_members 10 | // ignore_for_file: annotate_overrides 11 | 12 | class _$CounterActions extends CounterActions { 13 | factory _$CounterActions() => new _$CounterActions._(); 14 | _$CounterActions._() : super._(); 15 | 16 | final ActionDispatcher increment = 17 | new ActionDispatcher('CounterActions-increment'); 18 | final ActionDispatcher incrementOther = 19 | new ActionDispatcher('CounterActions-incrementOther'); 20 | 21 | @override 22 | void setDispatcher(Dispatcher dispatcher) { 23 | increment.setDispatcher(dispatcher); 24 | incrementOther.setDispatcher(dispatcher); 25 | } 26 | } 27 | 28 | class CounterActionsNames { 29 | static final ActionName increment = 30 | new ActionName('CounterActions-increment'); 31 | static final ActionName incrementOther = 32 | new ActionName('CounterActions-incrementOther'); 33 | } 34 | 35 | // ************************************************************************** 36 | // BuiltValueGenerator 37 | // ************************************************************************** 38 | 39 | // ignore_for_file: always_put_control_body_on_new_line 40 | // ignore_for_file: annotate_overrides 41 | // ignore_for_file: avoid_annotating_with_dynamic 42 | // ignore_for_file: avoid_catches_without_on_clauses 43 | // ignore_for_file: avoid_returning_this 44 | // ignore_for_file: lines_longer_than_80_chars 45 | // ignore_for_file: omit_local_variable_types 46 | // ignore_for_file: prefer_expression_function_bodies 47 | // ignore_for_file: sort_constructors_first 48 | // ignore_for_file: unnecessary_const 49 | // ignore_for_file: unnecessary_new 50 | 51 | class _$Counter extends Counter { 52 | @override 53 | final int count; 54 | @override 55 | final int other; 56 | 57 | factory _$Counter([void updates(CounterBuilder b)]) => 58 | (new CounterBuilder()..update(updates)).build(); 59 | 60 | _$Counter._({this.count, this.other}) : super._() { 61 | if (count == null) throw new BuiltValueNullFieldError('Counter', 'count'); 62 | if (other == null) throw new BuiltValueNullFieldError('Counter', 'other'); 63 | } 64 | 65 | @override 66 | Counter rebuild(void updates(CounterBuilder b)) => 67 | (toBuilder()..update(updates)).build(); 68 | 69 | @override 70 | CounterBuilder toBuilder() => new CounterBuilder()..replace(this); 71 | 72 | @override 73 | bool operator ==(Object other) { 74 | if (identical(other, this)) return true; 75 | return other is Counter && count == other.count && other == other.other; 76 | } 77 | 78 | @override 79 | int get hashCode { 80 | return $jf($jc($jc(0, count.hashCode), other.hashCode)); 81 | } 82 | 83 | @override 84 | String toString() { 85 | return (newBuiltValueToStringHelper('Counter') 86 | ..add('count', count) 87 | ..add('other', other)) 88 | .toString(); 89 | } 90 | } 91 | 92 | class CounterBuilder implements Builder { 93 | _$Counter _$v; 94 | 95 | int _count; 96 | int get count => _$this._count; 97 | set count(int count) => _$this._count = count; 98 | 99 | int _other; 100 | int get other => _$this._other; 101 | set other(int other) => _$this._other = other; 102 | 103 | CounterBuilder(); 104 | 105 | CounterBuilder get _$this { 106 | if (_$v != null) { 107 | _count = _$v.count; 108 | _other = _$v.other; 109 | _$v = null; 110 | } 111 | return this; 112 | } 113 | 114 | @override 115 | void replace(Counter other) { 116 | if (other == null) throw new ArgumentError.notNull('other'); 117 | _$v = other as _$Counter; 118 | } 119 | 120 | @override 121 | void update(void updates(CounterBuilder b)) { 122 | if (updates != null) updates(this); 123 | } 124 | 125 | @override 126 | _$Counter build() { 127 | final _$result = _$v ?? new _$Counter._(count: count, other: other); 128 | replace(_$result); 129 | return _$result; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/unit/test_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_redux/built_redux.dart'; 2 | import 'package:flutter_built_redux/flutter_built_redux.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'test_models.dart'; 6 | 7 | final providerKey = new Key('providerKey'); 8 | final counterKey = new Key('counterKey'); 9 | final incrementTextKey = new Key('incrementTextKey'); 10 | final incrementButtonKey = new Key('incrementButtonKey'); 11 | final incrementOtherButtonKey = new Key('incrementOtherButtonKey'); 12 | 13 | class ProviderWidgetConnector extends StatelessWidget { 14 | final Store store; 15 | 16 | ProviderWidgetConnector(this.store) : super(key: providerKey); 17 | 18 | @override 19 | Widget build(BuildContext context) => new MaterialApp( 20 | title: 'flutter_built_redux_test', 21 | home: new ReduxProvider( 22 | store: store, 23 | child: new CounterWidget(), 24 | ), 25 | ); 26 | } 27 | 28 | // ignore: must_be_immutable 29 | class ProviderWidgetConnection extends StatelessWidget { 30 | final Store store; 31 | int numBuilds = 0; 32 | 33 | ProviderWidgetConnection(this.store) : super(key: providerKey); 34 | 35 | @override 36 | Widget build(BuildContext context) => new MaterialApp( 37 | title: 'flutter_built_redux_test', 38 | home: new ReduxProvider( 39 | store: store, 40 | child: new StoreConnection( 41 | connect: (state) => state.count, 42 | key: counterKey, 43 | builder: (BuildContext context, int count, CounterActions actions) { 44 | numBuilds++; 45 | return new Scaffold( 46 | body: new Row( 47 | children: [ 48 | new RaisedButton( 49 | onPressed: actions.increment, 50 | child: new Text('Increment'), 51 | key: incrementButtonKey, 52 | ), 53 | new RaisedButton( 54 | onPressed: actions.incrementOther, 55 | child: new Text('Increment Other'), 56 | key: incrementOtherButtonKey, 57 | ), 58 | new Text( 59 | 'Count: $count', 60 | key: incrementTextKey, 61 | ), 62 | ], 63 | ), 64 | ); 65 | }, 66 | ), 67 | ), 68 | ); 69 | } 70 | 71 | // ignore: must_be_immutable 72 | class CounterWidget extends StoreConnector { 73 | // the number of times this component has been rebuild 74 | // used to test that we don't update after updating state 75 | // the connect function does not consume 76 | int numBuilds = 0; 77 | 78 | CounterWidget() : super(key: counterKey); 79 | 80 | @override 81 | int connect(Counter state) { 82 | return state.count; 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context, int count, CounterActions actions) { 87 | numBuilds++; 88 | return new Scaffold( 89 | body: new Row( 90 | children: [ 91 | new RaisedButton( 92 | onPressed: actions.increment, 93 | child: new Text('Increment'), 94 | key: incrementButtonKey, 95 | ), 96 | new RaisedButton( 97 | onPressed: actions.incrementOther, 98 | child: new Text('Increment Other'), 99 | key: incrementOtherButtonKey, 100 | ), 101 | new Text( 102 | 'Count: $count', 103 | key: incrementTextKey, 104 | ), 105 | ], 106 | ), 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tool/build.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:build_runner/build_runner.dart'; 4 | import 'package:built_value_generator/built_value_generator.dart'; 5 | import 'package:source_gen/source_gen.dart'; 6 | import 'package:built_redux/generator.dart'; 7 | 8 | /// Build the generated files in the built_value chat example. 9 | Future main(List args) async { 10 | await build([ 11 | new BuildAction( 12 | new PartBuilder([ 13 | new BuiltValueGenerator(), 14 | new BuiltReduxGenerator(), 15 | ]), 16 | 'flutter_built_redux', 17 | inputs: const ['test/unit/test_models.dart', 'example/example.dart']) 18 | ], deleteFilesByDefault: true); 19 | } 20 | --------------------------------------------------------------------------------