Skip to content

Commit 09395f8

Browse files
committed
Improve progress_control documentation and examples
- Update cancel() docs to explain it clears progress and statusMessage - Add factory variants table for async commands with progress - Fix Dio integration example to use try-finally pattern - Remove CommandBuilder example (incompatible with watchValue) - Remove subjective best practices - Improve batch processing example to show per-item progress
1 parent 24ab7e7 commit 09395f8

File tree

4 files changed

+68
-96
lines changed

4 files changed

+68
-96
lines changed

code_samples/lib/command_it/progress_batch_processing.dart

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,26 @@ import '_shared/stubs.dart';
55
final batchCommand = Command.createAsyncWithProgress<List<Item>, String>(
66
(items, handle) async {
77
final total = items.length;
8-
int processed = 0;
8+
int current = 0;
99

1010
for (final item in items) {
1111
if (handle.isCanceled.value) {
12-
return 'Canceled ($processed/$total processed)';
12+
return 'Canceled ($current/$total processed)';
1313
}
1414

15-
await processItem(item);
16-
processed++;
15+
current++;
16+
handle.updateStatusMessage('Processing item $current of $total');
1717

18-
handle.updateProgress(processed / total);
19-
handle.updateStatusMessage('Processed $processed of $total items');
18+
// Process item with per-item progress
19+
const steps = 10;
20+
for (int step = 0; step <= steps; step++) {
21+
if (handle.isCanceled.value) {
22+
return 'Canceled ($current/$total processed)';
23+
}
24+
25+
handle.updateProgress(step / steps);
26+
await simulateDelay(50); // Simulate work step
27+
}
2028
}
2129

2230
return 'Processed $total items';

code_samples/lib/command_it/progress_dio_integration.dart

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,18 @@ final downloadCommand = Command.createAsyncWithProgress<String, File>(
3636
final cancelToken = CancelToken();
3737

3838
// Forward command cancellation to Dio
39-
handle.isCanceled.listen((canceled, _) {
40-
if (canceled) cancelToken.cancel('User canceled');
41-
});
39+
late final subscription;
40+
subscription = handle.isCanceled.listen(
41+
(canceled, _) {
42+
if (canceled) {
43+
cancelToken.cancel('User canceled');
44+
subscription.cancel();
45+
}
46+
},
47+
);
4248

4349
try {
44-
final response = await dio.download(
50+
await dio.download(
4551
url,
4652
'/downloads/file.zip',
4753
cancelToken: cancelToken,
@@ -56,11 +62,8 @@ final downloadCommand = Command.createAsyncWithProgress<String, File>(
5662
},
5763
);
5864
return File('/downloads/file.zip');
59-
} on DioException catch (e) {
60-
if (CancelToken.isCancel(e)) {
61-
return File(''); // Handle cancellation
62-
}
63-
rethrow;
65+
} finally {
66+
subscription.cancel();
6467
}
6568
},
6669
initialValue: File(''),

code_samples/lib/command_it/progress_factory_variants.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
import 'package:command_it/command_it.dart';
22
import '_shared/stubs.dart';
33

4+
// #region main
5+
// Full signature: parameter + result
6+
final processCommand = Command.createAsyncWithProgress<int, String>(
7+
(count, handle) async {
8+
for (int i = 0; i < count; i++) {
9+
if (handle.isCanceled.value) return 'Canceled';
10+
11+
await processItem(Item());
12+
handle.updateProgress((i + 1) / count);
13+
handle.updateStatusMessage('Processing item ${i + 1} of $count');
14+
}
15+
return 'Processed $count items';
16+
},
17+
initialValue: '',
18+
);
19+
// #endregion main
20+
421
// #region example
522
// Full signature: parameter + result
623
final processCommand = Command.createAsyncWithProgress<int, String>(

docs/documentation/command_it/progress_control.md

Lines changed: 25 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,16 @@ Use the `WithProgress` factory variants to create commands that receive a `Progr
4343

4444
### Async Commands with Progress
4545

46-
<<< @/../code_samples/lib/command_it/progress_factory_variants.dart#example
46+
<<< @/../code_samples/lib/command_it/progress_factory_variants.dart#main
47+
48+
All four async variants are available:
49+
50+
| Factory Method | Function Signature |
51+
|---------------|-------------------|
52+
| `createAsyncWithProgress` | `(param, handle) async => TResult` |
53+
| `createAsyncNoParamWithProgress` | `(handle) async => TResult` |
54+
| `createAsyncNoResultWithProgress` | `(param, handle) async => void` |
55+
| `createAsyncNoParamNoResultWithProgress` | `(handle) async => void` |
4756

4857
### Undoable Commands with Progress
4958

@@ -137,7 +146,13 @@ if (isCanceled) Text('Operation canceled')
137146

138147
### cancel()
139148

140-
Request cooperative cancellation of the operation:
149+
Request cooperative cancellation of the operation. This method:
150+
151+
- Sets `isCanceled` to `true`
152+
- Clears `progress` to `0.0`
153+
- Clears `statusMessage` to `null`
154+
155+
This immediately clears progress state from the UI, providing instant visual feedback that the operation was canceled.
141156

142157
```dart
143158
// In UI:
@@ -185,7 +200,7 @@ if (command.progress.value == 1.0) {
185200
- Reset progress between manual executions
186201
- Prepare command state for testing
187202

188-
**Note:** Progress is automatically reset at the start of each `run()` execution, so manual resets are typically only needed for UI cleanup or resuming operations.
203+
**Note:** Progress is automatically reset at the start of each `run()` execution, so manual resets are typically only needed for UI cleanup or resuming operations. Additionally, calling `cancel()` also clears progress and statusMessage to provide immediate visual feedback.
189204

190205
## Integration Patterns
191206

@@ -209,33 +224,6 @@ The `isCanceled` property is a `ValueListenable`, allowing you to forward cancel
209224

210225
<<< @/../code_samples/lib/command_it/progress_dio_integration.dart#example
211226

212-
### With CommandBuilder
213-
214-
CommandBuilder automatically exposes progress properties for easy UI integration:
215-
216-
```dart
217-
CommandBuilder<File, String>(
218-
command: uploadCommand,
219-
whileRunning: (context, lastResult, param) {
220-
final progress = uploadCommand.progress.value;
221-
final status = uploadCommand.statusMessage.value;
222-
223-
return Column(
224-
children: [
225-
LinearProgressIndicator(value: progress),
226-
SizedBox(height: 8),
227-
Text(status ?? 'Processing...'),
228-
TextButton(
229-
onPressed: uploadCommand.cancel,
230-
child: Text('Cancel'),
231-
),
232-
],
233-
);
234-
},
235-
onData: (context, result, param) => Text('Result: $result'),
236-
)
237-
```
238-
239227
## Commands Without Progress
240228

241229
Commands created with regular factories (without `WithProgress`) still have progress properties, but they return default values:
@@ -250,13 +238,16 @@ final command = Command.createAsync<void, String>(
250238
command.progress.value // Always 0.0
251239
command.statusMessage.value // Always null
252240
command.isCanceled.value // Always false
253-
command.cancel() // Does nothing
241+
command.cancel() // No effect (no progress handle)
254242
```
255243

256244
This zero-overhead design means:
257-
- <ul style="list-style: none; padding-left: 0;"><li style="padding-left: 1.5em; text-indent: -1.5em;">✅ UI code can always access progress properties without null checks</li></ul>
258-
- <ul style="list-style: none; padding-left: 0;"><li style="padding-left: 1.5em; text-indent: -1.5em;">✅ No memory cost for commands that don't need progress</li></ul>
259-
- <ul style="list-style: none; padding-left: 0;"><li style="padding-left: 1.5em; text-indent: -1.5em;">✅ Easy to add progress to existing commands later (just change factory)</li></ul>
245+
246+
<ul style="list-style: none; padding-left: 0;">
247+
<li style="padding-left: 1.5em; text-indent: -1.5em;">✅ UI code can always access progress properties without null checks</li>
248+
<li style="padding-left: 1.5em; text-indent: -1.5em;">✅ No memory cost for commands that don't need progress</li>
249+
<li style="padding-left: 1.5em; text-indent: -1.5em;">✅ Easy to add progress to existing commands later (just change factory)</li>
250+
</ul>
260251

261252
## Testing with MockCommand
262253

@@ -296,53 +287,6 @@ for (final item in items) {
296287
}
297288
```
298289

299-
### DO: Provide meaningful status messages
300-
301-
```dart
302-
// ✅ Good - specific, actionable information
303-
handle.updateStatusMessage('Uploading file 3 of 10...');
304-
handle.updateStatusMessage('Processing image (1.2 MB)...');
305-
handle.updateStatusMessage('Verifying upload integrity...');
306-
```
307-
308-
### DON'T: Use vague status messages
309-
310-
```dart
311-
// ❌ Bad - not helpful to users
312-
handle.updateStatusMessage('Working...');
313-
handle.updateStatusMessage('Please wait...');
314-
```
315-
316-
### DO: Update progress accurately
317-
318-
```dart
319-
// ✅ Good - progress matches actual work done
320-
final total = items.length;
321-
for (int i = 0; i < total; i++) {
322-
await processItem(items[i]);
323-
handle.updateProgress((i + 1) / total); // Accurate progress
324-
}
325-
```
326-
327-
### DON'T: Set progress to 1.0 prematurely
328-
329-
```dart
330-
// ❌ Bad - progress at 100% but still working
331-
handle.updateProgress(1.0);
332-
await finalizeOperation(); // Still working after "100%"
333-
```
334-
335-
### DO: Clean up on cancellation
336-
337-
```dart
338-
// ✅ Good - cleanup resources on cancel
339-
if (handle.isCanceled.value) {
340-
await cleanupPartialUpload(uploadId);
341-
await deleteTemporaryFiles();
342-
return 'Canceled';
343-
}
344-
```
345-
346290
## Performance Considerations
347291

348292
**Progress updates are lightweight** - each update is just a ValueNotifier assignment. However, avoid excessive updates:

0 commit comments

Comments
 (0)