Skip to content

Commit e72c64e

Browse files
committed
Release v9.2.0: Add CommandBuilder auto-run and deprecate Command.toWidget()
New Features: - CommandBuilder now supports automatically executing commands on first build via runCommandOnFirstBuild parameter, eliminating StatefulWidget boilerplate for simple data loading scenarios (especially useful for non-watch_it users) - Added initialParam parameter to pass parameters when auto-running commands Deprecations: - Deprecated Command.toWidget() in favor of CommandResult.toWidget() which provides a richer API with better separation of concerns (onData/onSuccess/ onNullData). Removal planned for v10.0.0. Breaking Changes: - CommandBuilder is now a StatefulWidget instead of StatelessWidget (should not affect users as the widget API remains the same)
1 parent 9924678 commit e72c64e

File tree

5 files changed

+200
-16
lines changed

5 files changed

+200
-16
lines changed

CHANGELOG.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,49 @@
1+
[9.2.0] - 2025-11-22
2+
3+
### New Features
4+
5+
- **CommandBuilder Auto-Run**: CommandBuilder now supports automatically executing commands on first build via `runCommandOnFirstBuild` parameter. This is especially useful for non-watch_it users who want self-contained data-loading widgets without needing StatefulWidget boilerplate.
6+
7+
```dart
8+
CommandBuilder<String, List<Item>>(
9+
command: searchCommand,
10+
runCommandOnFirstBuild: true, // Executes in initState
11+
initialParam: 'flutter', // Parameter to pass
12+
onData: (context, items, _) => ItemList(items),
13+
whileRunning: (context, _, __) => LoadingIndicator(),
14+
)
15+
```
16+
17+
**Benefits:**
18+
- Eliminates StatefulWidget boilerplate for simple data loading
19+
- Self-contained widgets that manage their own data fetching
20+
- Runs only once in initState (not on rebuilds)
21+
- Perfect for non-watch_it users (watch_it users should continue using `callOnce`)
22+
23+
### Deprecations
24+
25+
- **Deprecated Command.toWidget()**: The `Command.toWidget()` method is now deprecated in favor of `CommandResult.toWidget()`. The CommandResult version provides a richer API with better separation of concerns (onData/onSuccess/onNullData) and is already used by CommandBuilder. Command.toWidget() will be removed in v10.0.0.
26+
27+
**Migration:**
28+
```dart
29+
// Before (deprecated):
30+
command.toWidget(
31+
onResult: (data, param) => DataWidget(data),
32+
whileRunning: (lastData, param) => LoadingWidget(),
33+
onError: (error, param) => ErrorWidget(error),
34+
)
35+
36+
// After (recommended):
37+
ValueListenableBuilder(
38+
valueListenable: command.results,
39+
builder: (context, result, _) => result.toWidget(
40+
onData: (data, param) => DataWidget(data),
41+
whileRunning: (lastData, param) => LoadingWidget(),
42+
onError: (error, lastData, param) => ErrorWidget(error),
43+
),
44+
)
45+
```
46+
147
[9.1.1] - 2025-11-21
248

349
### Improvements

lib/command_builder.dart

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
part of command_it;
22

3-
class CommandBuilder<TParam, TResult> extends StatelessWidget {
3+
class CommandBuilder<TParam, TResult> extends StatefulWidget {
44
final Command<TParam, TResult> command;
55

66
/// This builder will be called when the
@@ -40,6 +40,17 @@ class CommandBuilder<TParam, TResult> extends StatelessWidget {
4040
TParam?,
4141
)? onError;
4242

43+
/// If true, the command will be executed once when the widget is first built.
44+
/// This is useful for loading data when the widget is mounted, especially when
45+
/// not using watch_it (which provides `callOnce` for this purpose).
46+
///
47+
/// The command will only run once in initState, not on subsequent rebuilds.
48+
final bool runCommandOnFirstBuild;
49+
50+
/// The parameter to pass to the command when [runCommandOnFirstBuild] is true.
51+
/// Ignored if [runCommandOnFirstBuild] is false.
52+
final TParam? initialParam;
53+
4354
const CommandBuilder({
4455
required this.command,
4556
this.onSuccess,
@@ -53,41 +64,60 @@ class CommandBuilder<TParam, TResult> extends StatelessWidget {
5364
)
5465
this.whileExecuting,
5566
this.onError,
67+
this.runCommandOnFirstBuild = false,
68+
this.initialParam,
5669
super.key,
5770
});
5871

72+
@override
73+
State<CommandBuilder<TParam, TResult>> createState() =>
74+
_CommandBuilderState<TParam, TResult>();
75+
}
76+
77+
class _CommandBuilderState<TParam, TResult>
78+
extends State<CommandBuilder<TParam, TResult>> {
79+
@override
80+
void initState() {
81+
super.initState();
82+
if (widget.runCommandOnFirstBuild) {
83+
widget.command(widget.initialParam);
84+
}
85+
}
86+
5987
@override
6088
Widget build(BuildContext context) {
61-
if (command._noReturnValue) {}
89+
if (widget.command._noReturnValue) {}
6290
return ValueListenableBuilder<CommandResult<TParam?, TResult>>(
63-
valueListenable: command.results,
91+
valueListenable: widget.command.results,
6492
builder: (context, result, _) {
6593
return result.toWidget(
66-
onData: onData != null
67-
? (data, paramData) => onData!.call(context, data, paramData)
94+
onData: widget.onData != null
95+
? (data, paramData) =>
96+
widget.onData!.call(context, data, paramData)
6897
: null,
69-
onSuccess: onSuccess != null
70-
? (paramData) => onSuccess!.call(context, paramData)
98+
onSuccess: widget.onSuccess != null
99+
? (paramData) => widget.onSuccess!.call(context, paramData)
71100
: null,
72-
onNullData: onNullData != null
73-
? (paramData) => onNullData!.call(context, paramData)
101+
onNullData: widget.onNullData != null
102+
? (paramData) => widget.onNullData!.call(context, paramData)
74103
: null,
75104
// ignore: deprecated_member_use_from_same_package
76-
whileRunning: (whileRunning ?? whileExecuting) != null
105+
whileRunning: (widget.whileRunning ?? widget.whileExecuting) != null
77106
// ignore: deprecated_member_use_from_same_package
78-
? (lastData, paramData) => (whileRunning ?? whileExecuting)!
79-
.call(context, lastData, paramData)
107+
? (lastData, paramData) =>
108+
(widget.whileRunning ?? widget.whileExecuting)!
109+
.call(context, lastData, paramData)
80110
: null,
81111
onError: (error, lastData, paramData) {
82-
if (onError == null) {
112+
if (widget.onError == null) {
83113
return const SizedBox();
84114
}
85115
assert(
86116
result.errorReaction?.shouldCallLocalHandler == true,
87-
'This CommandBuilder received an error from Command ${command.name} '
117+
'This CommandBuilder received an error from Command ${widget.command.name} '
88118
'but the errorReaction indidates that the error should not be handled locally. ',
89119
);
90-
return onError!.call(context, error, lastData, paramData);
120+
return widget.onError!.call(context, error, lastData, paramData);
91121
},
92122
);
93123
},

lib/command_it.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,10 @@ abstract class Command<TParam, TResult> extends CustomValueNotifier<TResult> {
755755
/// Returns a the result of one of three builders depending on the current state
756756
/// of the Command. This function won't trigger a rebuild if the command changes states
757757
/// so it should be used together with get_it_mixin, provider, flutter_hooks and the like.
758+
@Deprecated(
759+
'Use CommandResult.toWidget() instead. '
760+
'This will be removed in v10.0.0.',
761+
)
758762
Widget toWidget({
759763
required Widget Function(TResult lastResult, TParam? param) onResult,
760764
Widget Function(TResult lastResult, TParam? param)? whileRunning,

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.1.1
3+
version: 9.2.0
44
homepage: https://github.com/flutter-it/command_it
55

66
screenshots:

test/flutter_command_test.dart

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,6 +1375,110 @@ void main() {
13751375

13761376
expect(find.text('Success!'), findsOneWidget);
13771377
});
1378+
1379+
testWidgets('Test CommandBuilder with runCommandOnFirstBuild=false',
1380+
(WidgetTester tester) async {
1381+
var executionCount = 0;
1382+
final testCommand = Command.createAsyncNoParamNoResult(() async {
1383+
executionCount++;
1384+
await Future<void>.delayed(const Duration(milliseconds: 100));
1385+
});
1386+
1387+
await tester.pumpWidget(
1388+
MaterialApp(
1389+
home: Scaffold(
1390+
body: CommandBuilder<void, void>(
1391+
command: testCommand,
1392+
runCommandOnFirstBuild: false, // Should not run
1393+
onSuccess: (context, _) => const Text('Success!'),
1394+
whileRunning: (context, _, __) => const Text('Loading'),
1395+
),
1396+
),
1397+
),
1398+
);
1399+
1400+
await tester.pump();
1401+
1402+
// Command should not have executed
1403+
expect(executionCount, 0);
1404+
expect(find.text('Loading'), findsNothing);
1405+
});
1406+
1407+
testWidgets(
1408+
'Test CommandBuilder with runCommandOnFirstBuild=true (no param)',
1409+
(WidgetTester tester) async {
1410+
var executionCount = 0;
1411+
final testCommand = Command.createAsyncNoParamNoResult(() async {
1412+
executionCount++;
1413+
await Future<void>.delayed(const Duration(milliseconds: 100));
1414+
});
1415+
1416+
await tester.pumpWidget(
1417+
MaterialApp(
1418+
home: Scaffold(
1419+
body: CommandBuilder<void, void>(
1420+
command: testCommand,
1421+
runCommandOnFirstBuild: true,
1422+
onSuccess: (context, _) => const Text('Success!'),
1423+
whileRunning: (context, _, __) => const Text('Loading'),
1424+
),
1425+
),
1426+
),
1427+
);
1428+
1429+
// Pump to allow initState to run and command to start
1430+
await tester.pump(const Duration(milliseconds: 10));
1431+
1432+
// Command should have executed once
1433+
expect(executionCount, 1);
1434+
expect(find.text('Loading'), findsOneWidget);
1435+
1436+
// Wait for command to complete
1437+
await tester.pump(const Duration(milliseconds: 200));
1438+
1439+
expect(find.text('Success!'), findsOneWidget);
1440+
expect(executionCount, 1); // Should still be 1 (not run again)
1441+
});
1442+
1443+
testWidgets(
1444+
'Test CommandBuilder with runCommandOnFirstBuild=true and initialParam',
1445+
(WidgetTester tester) async {
1446+
String? receivedParam;
1447+
final testCommand = Command.createAsync<String, String>(
1448+
(param) async {
1449+
receivedParam = param;
1450+
await Future<void>.delayed(const Duration(milliseconds: 100));
1451+
return 'Result: $param';
1452+
},
1453+
initialValue: '', // Named parameter
1454+
);
1455+
1456+
await tester.pumpWidget(
1457+
MaterialApp(
1458+
home: Scaffold(
1459+
body: CommandBuilder<String, String>(
1460+
command: testCommand,
1461+
runCommandOnFirstBuild: true,
1462+
initialParam: 'test-param',
1463+
onData: (context, data, _) => Text(data),
1464+
whileRunning: (context, _, __) => const Text('Loading'),
1465+
),
1466+
),
1467+
),
1468+
);
1469+
1470+
// Pump to allow initState to run and command to start
1471+
await tester.pump(const Duration(milliseconds: 10));
1472+
1473+
// Command should have been called with the param
1474+
expect(receivedParam, 'test-param');
1475+
expect(find.text('Loading'), findsOneWidget);
1476+
1477+
// Wait for command to complete
1478+
await tester.pump(const Duration(milliseconds: 200));
1479+
1480+
expect(find.text('Result: test-param'), findsOneWidget);
1481+
});
13781482
});
13791483
group('UndoableCommand', () {
13801484
test(

0 commit comments

Comments
 (0)