diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 3d5cacc..f4568ea 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -8,7 +8,7 @@ if (localPropertiesFile.exists()) { def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") + throw new org.gradle.api.GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') diff --git a/example/android/build.gradle b/example/android/build.gradle index 3cdaac9..ea855b6 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/example/lib/main_is_waiting_works_when_state_unchanged.dart b/example/lib/main_is_waiting_works_when_state_unchanged.dart new file mode 100644 index 0000000..acc3e4a --- /dev/null +++ b/example/lib/main_is_waiting_works_when_state_unchanged.dart @@ -0,0 +1,146 @@ +import 'package:async_redux/async_redux.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + var store = Store(initialState: AppState(counter: 0)); + store.onChange.listen(print); + + return MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: StoreProvider( + store: store, + child: const MyHomePage(), + ), + ); + } +} + +class MyHomePage extends StatelessWidget { + const MyHomePage({super.key}); + + @override + Widget build(BuildContext context) { + return StoreConnector( + vm: () => CounterVmFactory(), + shouldUpdateModel: (s) => s.counter >= 0, + builder: (context, vm) { + return MyHomePageContent( + title: 'IsWaiting works when state unchanged', + counter: vm.counter, + isIncrementing: vm.isIncrementing, + increment: vm.increment, + ); + }, + ); + } +} + +class MyHomePageContent extends StatelessWidget { + const MyHomePageContent({ + super.key, + required this.title, + required this.counter, + required this.isIncrementing, + required this.increment, + }); + + final String title; + final int counter; + final bool isIncrementing; + final VoidCallback increment; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('You pushed the button:'), + Text( + '$counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: isIncrementing ? null : increment, + elevation: isIncrementing ? 0 : 6, + backgroundColor: isIncrementing ? Colors.grey[300] : Colors.blue, + child: isIncrementing ? const Padding( + padding: const EdgeInsets.all(16.0), + child: const CircularProgressIndicator(), + ) : const Icon(Icons.add), + ), + ); + } +} + +class AppState { + final int counter; + + AppState({required this.counter}); + + AppState copy({int? counter}) => AppState(counter: counter ?? this.counter); + + @override + String toString() { + return '.\n.\n.\nAppState{counter: $counter}\n.\n.\n'; + } +} + +class CounterVm extends Vm { + final int counter; + final bool isIncrementing; + final VoidCallback increment; + + CounterVm({ + required this.counter, + required this.isIncrementing, + required this.increment, + }) : super(equals: [ + counter, + isIncrementing, + ]); +} + +class CounterVmFactory extends VmFactory { + @override + CounterVm fromStore() => CounterVm( + counter: state.counter, + isIncrementing: isWaiting(IncrementAction), + increment: () => dispatch(IncrementAction()), + ); +} + +class IncrementAction extends ReduxAction { + @override + Future reduce() async { + dispatch(DoIncrementAction()); + await Future.delayed(const Duration(milliseconds: 1250)); + return null; + } +} + +class DoIncrementAction extends ReduxAction { + @override + AppState? reduce() { + return AppState(counter: state.counter + 1); + } +} diff --git a/lib/src/store.dart b/lib/src/store.dart index 024b30b..2e36386 100644 --- a/lib/src/store.dart +++ b/lib/src/store.dart @@ -1988,6 +1988,18 @@ class Store { return new UnmodifiableSetView(this._actionsInProgress); } + /// Returns a copy of the set of actions on progress. + Set> copyActionsInProgress() => + HashSet>.identity()..addAll(actionsInProgress()); + + /// Returns true if the actions in progress are equal to the given set. + bool actionsInProgressEqualTo(Set> set) { + if (set.length != _actionsInProgress.length) { + return false; + } + return set.containsAll(_actionsInProgress) && _actionsInProgress.containsAll(set); + } + /// Actions that we may put into [_actionsInProgress]. /// This helps to know when to rebuild to make [isWaiting] work. final Set _awaitableActions = HashSet.identity(); diff --git a/lib/src/store_provider_and_connector.dart b/lib/src/store_provider_and_connector.dart index 1dcef70..1e547c0 100644 --- a/lib/src/store_provider_and_connector.dart +++ b/lib/src/store_provider_and_connector.dart @@ -4,6 +4,7 @@ // For more info, see: https://pub.dartlang.org/packages/async_redux import 'dart:async'; +import 'dart:collection'; import 'package:async_redux/async_redux.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -410,7 +411,20 @@ class _StoreStreamListenerState // // This prevents unnecessary calculations of the view-model. bool _stateChanged(St state) { - return !identical(_mostRecentValidState, widget.store.state); + return !identical(_mostRecentValidState, widget.store.state) || _actionsInProgressHaveChanged(); + } + + /// Used by [_actionsInProgressHaveChanged]. + Set> _lastActionsInProgress = HashSet>.identity(); + + /// Returns true if the actions in progress have changed since the last time we checked. + bool _actionsInProgressHaveChanged() { + if (widget.store.actionsInProgressEqualTo(_lastActionsInProgress)) + return false; + else { + _lastActionsInProgress = widget.store.copyActionsInProgress(); + return true; + } } // If `shouldUpdateModel` is provided, it will calculate if the STORE state contains @@ -1041,7 +1055,7 @@ class StoreProvider extends InheritedWidget { } } -/// Is an UNTYPED inherited widget used by `dispatch`, `dispatchAndWait` and `dispatchSync`. +/// An UNTYPED inherited widget used by `dispatch`, `dispatchAndWait` and `dispatchSync`. /// That's useful because they can dispatch without the knowing the St type, but it DOES NOT /// REBUILD. class _InheritedUntypedDoesNotRebuild extends InheritedWidget { @@ -1065,7 +1079,7 @@ class _InheritedUntypedDoesNotRebuild extends InheritedWidget { } } -/// is a StatefulWidget that listens to the store (onChange) and +/// A StatefulWidget that listens to the store (onChange) and /// rebuilds the whenever there is a new state available. class _WidgetListensOnChange extends StatefulWidget { final Widget child; @@ -1095,7 +1109,7 @@ class _WidgetListensOnChangeState extends State<_WidgetListensOnChange> { // Make sure we're not rebuilding if the state didn't change. // Note: This is not necessary because the store only sends the new state if it changed: - // `if (state != null && !identical(_state, state)) { ... }` + // `if (((state != null) && !identical(_state, state)) ...` // I'm leaving it here because in the future I want to improve this by only rebuilding // when the part of the state that the widgets depend on changes. // To implement that in the future I have to create some special InheritedWidget that @@ -1117,7 +1131,7 @@ class _WidgetListensOnChangeState extends State<_WidgetListensOnChange> { } } -/// Is an UNTYPED inherited widget that is used by `isWaiting`, `isFailed` and `exceptionFor`. +/// An UNTYPED inherited widget that is used by `isWaiting`, `isFailed` and `exceptionFor`. /// That's useful because these methods can find it without the knowing the St type, but /// it REBUILDS. Note: `_InheritedUntypedRebuilds._isOn` is true only after `state`, `isWaiting`, /// `isFailed` and `exceptionFor` are used for the first time. This is to make it faster by diff --git a/pubspec.yaml b/pubspec.yaml index 3103789..cda3b3a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ topics: - testing environment: - sdk: '>=3.2.0 <4.0.0' + sdk: '>=3.5.0 <4.0.0' flutter: ">=3.16.0" dependencies: