Skip to content

Commit 304f3b5

Browse files
committed
Fix progress state not resetting between executions (v9.4.1)
- Add ProgressHandle.reset() to clear all state - Wrap WithProgress factory functions to auto-reset before execution - Update cancel() to also clear progress and statusMessage - Add Command.resetProgress() for manual resets - Fix tests to account for cancel() clearing progress/status Fixes issue where canceled commands remained canceled on next run.
1 parent 75b39fa commit 304f3b5

File tree

5 files changed

+200
-32
lines changed

5 files changed

+200
-32
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
[9.4.1] - 2024-11-24
2+
3+
### Bug Fixes
4+
5+
- **Progress Control**: Fixed progress state not resetting between command executions
6+
- Progress state (progress, statusMessage, isCanceled) now automatically resets at the start of each execution
7+
- `cancel()` method now also clears progress and statusMessage (not just sets isCanceled flag)
8+
- Prevents issues where canceled commands would remain canceled on subsequent runs
9+
- Implementation uses wrapper functions in WithProgress factories to call `reset()` automatically
10+
11+
**New Methods**:
12+
- `Command.resetProgress()` - Manually reset progress state (useful for clearing 100% progress from UI)
13+
14+
**Technical Details**:
15+
- Added `ProgressHandle.reset()` method to clear all state back to initial values
16+
- All 8 WithProgress factory methods now wrap user functions to call `reset()` before execution
17+
- `cancel()` now sets isCanceled=true, progress=0.0, and statusMessage=null for clean UI state
18+
119
[9.4.0] - 2024-11-24
220

321
### New Features

lib/command_it.dart

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,25 @@ abstract class Command<TParam, TResult> extends CustomValueNotifier<TResult> {
619619
/// The function must check `handle.isCanceled.value` and decide how to handle it.
620620
void cancel() => _handle?.cancel();
621621

622+
/// Manually resets all progress state to initial values.
623+
///
624+
/// Clears progress (to 0.0), statusMessage (to null), and isCanceled (to false).
625+
/// This is called automatically at the start of each execution, but can also
626+
/// be called manually when needed (e.g., to clear 100% progress from UI after
627+
/// completion).
628+
///
629+
/// Example:
630+
/// ```dart
631+
/// // After successful completion, reset progress for next run
632+
/// if (command.progress.value == 1.0) {
633+
/// await Future.delayed(Duration(seconds: 2));
634+
/// command.resetProgress();
635+
/// }
636+
/// ```
637+
///
638+
/// For commands without progress (created with regular factories), this is a no-op.
639+
void resetProgress() => _handle?.reset();
640+
622641
/// optional hander that will get called on any exception that happens inside
623642
/// any Command of the app. Ideal for logging.
624643
/// the [name] of the Command that was responsible for the error is inside
@@ -1877,8 +1896,11 @@ abstract class Command<TParam, TResult> extends CustomValueNotifier<TResult> {
18771896
// Create the ProgressHandle
18781897
final handle = ProgressHandle();
18791898

1880-
// Wrap the user's function to inject the handle
1881-
Future<TResult> wrappedFunc(TParam param) => func(param, handle);
1899+
// Wrap the user's function to reset handle state and inject the handle
1900+
Future<TResult> wrappedFunc(TParam param) async {
1901+
handle.reset(); // Reset progress state before each execution
1902+
return await func(param, handle);
1903+
}
18821904

18831905
// Create the command with the wrapped function
18841906
final command = CommandAsync<TParam, TResult>(
@@ -1935,8 +1957,11 @@ abstract class Command<TParam, TResult> extends CustomValueNotifier<TResult> {
19351957
// Create the ProgressHandle
19361958
final handle = ProgressHandle();
19371959

1938-
// Wrap the user's function to inject the handle
1939-
Future<TResult> wrappedFunc() => func(handle);
1960+
// Wrap the user's function to reset handle state and inject the handle
1961+
Future<TResult> wrappedFunc() async {
1962+
handle.reset(); // Reset progress state before each execution
1963+
return await func(handle);
1964+
}
19401965

19411966
// Create the command with the wrapped function
19421967
final command = CommandAsync<void, TResult>(
@@ -1987,8 +2012,11 @@ abstract class Command<TParam, TResult> extends CustomValueNotifier<TResult> {
19872012
// Create the ProgressHandle
19882013
final handle = ProgressHandle();
19892014

1990-
// Wrap the user's action to inject the handle
1991-
Future<void> wrappedAction(TParam param) => action(param, handle);
2015+
// Wrap the user's action to reset handle state and inject the handle
2016+
Future<void> wrappedAction(TParam param) async {
2017+
handle.reset(); // Reset progress state before each execution
2018+
return await action(param, handle);
2019+
}
19922020

19932021
// Create the command with the wrapped action
19942022
final command = CommandAsync<TParam, void>(
@@ -2039,8 +2067,11 @@ abstract class Command<TParam, TResult> extends CustomValueNotifier<TResult> {
20392067
// Create the ProgressHandle
20402068
final handle = ProgressHandle();
20412069

2042-
// Wrap the user's action to inject the handle
2043-
Future<void> wrappedAction() => action(handle);
2070+
// Wrap the user's action to reset handle state and inject the handle
2071+
Future<void> wrappedAction() async {
2072+
handle.reset(); // Reset progress state before each execution
2073+
return await action(handle);
2074+
}
20442075

20452076
// Create the command with the wrapped action
20462077
final command = CommandAsync<void, void>(
@@ -2113,10 +2144,12 @@ abstract class Command<TParam, TResult> extends CustomValueNotifier<TResult> {
21132144
// Create the ProgressHandle
21142145
final handle = ProgressHandle();
21152146

2116-
// Wrap the user's function to inject the handle
2147+
// Wrap the user's function to reset handle state and inject the handle
21172148
Future<TResult> wrappedFunc(
2118-
TParam param, UndoStack<TUndoState> undoStack) =>
2119-
func(param, handle, undoStack);
2149+
TParam param, UndoStack<TUndoState> undoStack) async {
2150+
handle.reset(); // Reset progress state before each execution
2151+
return await func(param, handle, undoStack);
2152+
}
21202153

21212154
// Create the command with the wrapped function
21222155
final command = UndoableCommand<TParam, TResult, TUndoState>(
@@ -2184,9 +2217,11 @@ abstract class Command<TParam, TResult> extends CustomValueNotifier<TResult> {
21842217
// Create the ProgressHandle
21852218
final handle = ProgressHandle();
21862219

2187-
// Wrap the user's function to inject the handle
2188-
Future<TResult> wrappedFunc(UndoStack<TUndoState> undoStack) =>
2189-
func(handle, undoStack);
2220+
// Wrap the user's function to reset handle state and inject the handle
2221+
Future<TResult> wrappedFunc(UndoStack<TUndoState> undoStack) async {
2222+
handle.reset(); // Reset progress state before each execution
2223+
return await func(handle, undoStack);
2224+
}
21902225

21912226
// Create the command with the wrapped function
21922227
final command = UndoableCommand<void, TResult, TUndoState>(
@@ -2248,9 +2283,12 @@ abstract class Command<TParam, TResult> extends CustomValueNotifier<TResult> {
22482283
// Create the ProgressHandle
22492284
final handle = ProgressHandle();
22502285

2251-
// Wrap the user's action to inject the handle
2252-
Future<void> wrappedAction(TParam param, UndoStack<TUndoState> undoStack) =>
2253-
action(param, handle, undoStack);
2286+
// Wrap the user's action to reset handle state and inject the handle
2287+
Future<void> wrappedAction(
2288+
TParam param, UndoStack<TUndoState> undoStack) async {
2289+
handle.reset(); // Reset progress state before each execution
2290+
return await action(param, handle, undoStack);
2291+
}
22542292

22552293
// Create the command with the wrapped action
22562294
final command = UndoableCommand<TParam, void, TUndoState>(
@@ -2313,9 +2351,11 @@ abstract class Command<TParam, TResult> extends CustomValueNotifier<TResult> {
23132351
// Create the ProgressHandle
23142352
final handle = ProgressHandle();
23152353

2316-
// Wrap the user's action to inject the handle
2317-
Future<void> wrappedAction(UndoStack<TUndoState> undoStack) =>
2318-
action(handle, undoStack);
2354+
// Wrap the user's action to reset handle state and inject the handle
2355+
Future<void> wrappedAction(UndoStack<TUndoState> undoStack) async {
2356+
handle.reset(); // Reset progress state before each execution
2357+
return await action(handle, undoStack);
2358+
}
23192359

23202360
// Create the command with the wrapped action
23212361
final command = UndoableCommand<void, void, TUndoState>(

lib/progress_handle.dart

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,32 @@ class ProgressHandle {
7777

7878
/// Requests cooperative cancellation of the operation.
7979
///
80-
/// Sets the [isCanceled] flag to true. The wrapped command function
81-
/// is responsible for checking this flag and responding appropriately.
80+
/// Sets the [isCanceled] flag to true and clears progress/status state.
81+
/// The wrapped command function is responsible for checking this flag
82+
/// and responding appropriately.
8283
///
8384
/// This does NOT forcibly stop execution - cancellation is cooperative.
8485
/// The function must check `isCanceled.value` and decide how to handle it.
8586
void cancel() {
8687
_isCanceled.value = true;
88+
_progress.value = 0.0;
89+
_statusMessage.value = null;
90+
}
91+
92+
/// Resets all progress state to initial values.
93+
///
94+
/// Called automatically at the start of each command execution.
95+
/// Resets:
96+
/// - [progress] to 0.0
97+
/// - [statusMessage] to null
98+
/// - [isCanceled] to false
99+
///
100+
/// This ensures each command execution starts with clean state,
101+
/// especially important for the cancellation flag.
102+
void reset() {
103+
_progress.value = 0.0;
104+
_statusMessage.value = null;
105+
_isCanceled.value = false;
87106
}
88107

89108
/// Disposes all internal notifiers.

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: command_it
22
description: command_it is a way to manage your state based on `ValueListenable` and the `Command` design pattern. It is a rebranding of flutter_command.
3-
version: 9.4.0
3+
version: 9.4.1
44
homepage: https://github.com/flutter-it/command_it
55

66
screenshots:

test/progress_handle_test.dart

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,28 +93,75 @@ void main() {
9393
expect(statusCollector.values, ['Loading...', 'Processing...', null]);
9494
});
9595

96-
test('cancel sets isCanceled and notifies', () {
96+
test('cancel sets isCanceled and clears progress/status', () {
97+
// Set up some state first
98+
handle.updateProgress(0.75);
99+
handle.updateStatusMessage('Almost done');
100+
101+
expect(handle.progress.value, 0.75);
102+
expect(handle.statusMessage.value, 'Almost done');
97103
expect(handle.isCanceled.value, false);
98104

105+
// Clear collectors to track cancel changes
106+
progressCollector.clear();
107+
statusCollector.clear();
108+
canceledCollector.clear();
109+
110+
// Cancel
99111
handle.cancel();
112+
113+
// Verify all state was cleared
100114
expect(handle.isCanceled.value, true);
115+
expect(handle.progress.value, 0.0);
116+
expect(handle.statusMessage.value, null);
117+
118+
// Verify listeners were notified
101119
expect(canceledCollector.values, [true]);
120+
expect(progressCollector.values, [0.0]);
121+
expect(statusCollector.values, [null]);
122+
});
102123

103-
// Calling cancel again is idempotent (value doesn't change, may not notify)
104-
handle.cancel();
105-
expect(handle.isCanceled.value, true);
106-
// Listener may or may not fire again for same value - just verify it's canceled
124+
test('reset clears all state back to initial values', () {
125+
// Set up some state
126+
handle.updateProgress(0.75);
127+
handle.updateStatusMessage('Almost done');
128+
handle.cancel(); // This sets isCanceled=true and clears progress/status
129+
130+
// Set progress again so reset() causes a change notification
131+
handle.updateProgress(0.5);
132+
handle.updateStatusMessage('Restarting...');
133+
134+
expect(handle.progress.value, 0.5);
135+
expect(handle.statusMessage.value, 'Restarting...');
107136
expect(handle.isCanceled.value, true);
137+
138+
// Clear collectors to verify reset notifications
139+
progressCollector.clear();
140+
statusCollector.clear();
141+
canceledCollector.clear();
142+
143+
// Reset
144+
handle.reset();
145+
146+
// Verify all values reset to initial state
147+
expect(handle.progress.value, 0.0);
148+
expect(handle.statusMessage.value, null);
149+
expect(handle.isCanceled.value, false);
150+
151+
// Verify listeners were notified of changes
152+
expect(progressCollector.values, [0.0]);
153+
expect(statusCollector.values, [null]);
154+
expect(canceledCollector.values, [false]);
108155
});
109156

110157
test('dispose cleans up all notifiers', () {
111158
handle.updateProgress(0.5);
112159
handle.updateStatusMessage('Test');
113-
handle.cancel();
160+
handle.cancel(); // cancel() also clears progress/status
114161

115162
// Verify values were collected before dispose
116-
expect(progressCollector.values, [0.5]);
117-
expect(statusCollector.values, ['Test']);
163+
expect(progressCollector.values, [0.5, 0.0]); // cancel() resets progress
164+
expect(statusCollector.values, ['Test', null]); // cancel() resets status
118165
expect(canceledCollector.values, [true]);
119166

120167
handle.dispose();
@@ -190,12 +237,56 @@ void main() {
190237
async.elapse(const Duration(milliseconds: 1000));
191238

192239
expect(command.value, 'Canceled');
193-
expect(command.progress.value, lessThan(1.0));
240+
expect(command.progress.value, 0.0); // Cancel clears progress
241+
expect(command.statusMessage.value, null); // Cancel clears status
242+
expect(command.isCanceled.value, true);
194243

195244
command.dispose();
196245
});
197246
});
198247

248+
test('progress state resets automatically on new run', () async {
249+
int executionCount = 0;
250+
251+
final command = Command.createAsyncWithProgress<void, String>(
252+
(_, handle) async {
253+
executionCount++;
254+
handle.updateProgress(0.5);
255+
handle.updateStatusMessage('Running execution $executionCount');
256+
await Future<void>.delayed(const Duration(milliseconds: 10));
257+
return 'Execution $executionCount complete';
258+
},
259+
initialValue: '',
260+
);
261+
262+
// First run
263+
await command.runAsync();
264+
expect(command.value, 'Execution 1 complete');
265+
expect(command.progress.value, 0.5);
266+
expect(command.statusMessage.value, 'Running execution 1');
267+
268+
// Second run - should automatically reset before execution
269+
await command.runAsync();
270+
expect(command.value, 'Execution 2 complete');
271+
expect(command.progress.value, 0.5);
272+
expect(command.statusMessage.value, 'Running execution 2');
273+
274+
// Cancel and verify state is cleared
275+
command.cancel();
276+
expect(command.progress.value, 0.0);
277+
expect(command.statusMessage.value, null);
278+
expect(command.isCanceled.value, true);
279+
280+
// Third run - should reset cancel flag and allow execution
281+
await command.runAsync();
282+
expect(command.value, 'Execution 3 complete');
283+
expect(command.progress.value, 0.5);
284+
expect(command.statusMessage.value, 'Running execution 3');
285+
expect(command.isCanceled.value, false); // Reset on new run
286+
287+
command.dispose();
288+
});
289+
199290
test('default progress returns 0.0 for commands without handle', () {
200291
final command = Command.createAsync<void, String>(
201292
(_) async => 'Done',

0 commit comments

Comments
 (0)