Skip to content

Commit

Permalink
isWaiting fix when action finishes but state hasn't changed.
Browse files Browse the repository at this point in the history
  • Loading branch information
marcglasberg committed Sep 6, 2024
1 parent f91441c commit e2a1e58
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 8 deletions.
2 changes: 1 addition & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion example/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
Expand Down
146 changes: 146 additions & 0 deletions example/lib/main_is_waiting_works_when_state_unchanged.dart
Original file line number Diff line number Diff line change
@@ -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<AppState>(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<AppState, CounterVm>(
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<AppState, MyHomePage, CounterVm> {
@override
CounterVm fromStore() => CounterVm(
counter: state.counter,
isIncrementing: isWaiting(IncrementAction),
increment: () => dispatch(IncrementAction()),
);
}

class IncrementAction extends ReduxAction<AppState> {
@override
Future<AppState?> reduce() async {
dispatch(DoIncrementAction());
await Future.delayed(const Duration(milliseconds: 1250));
return null;
}
}

class DoIncrementAction extends ReduxAction<AppState> {
@override
AppState? reduce() {
return AppState(counter: state.counter + 1);
}
}
12 changes: 12 additions & 0 deletions lib/src/store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1988,6 +1988,18 @@ class Store<St> {
return new UnmodifiableSetView(this._actionsInProgress);
}

/// Returns a copy of the set of actions on progress.
Set<ReduxAction<St>> copyActionsInProgress() =>
HashSet<ReduxAction<St>>.identity()..addAll(actionsInProgress());

/// Returns true if the actions in progress are equal to the given set.
bool actionsInProgressEqualTo(Set<ReduxAction<St>> 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<Type> _awaitableActions = HashSet<Type>.identity();
Expand Down
24 changes: 19 additions & 5 deletions lib/src/store_provider_and_connector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -410,7 +411,20 @@ class _StoreStreamListenerState<St, Model> //

// 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<ReduxAction<St>> _lastActionsInProgress = HashSet<ReduxAction<St>>.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
Expand Down Expand Up @@ -1041,7 +1055,7 @@ class StoreProvider<St> 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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit e2a1e58

Please sign in to comment.