Command Builders
Simplify command UI integration with CommandBuilder - a widget that handles all command states (loading, data, error) with minimal boilerplate.
Why Use CommandBuilder?
Instead of manually building ValueListenableBuilder widgets for command.results, use CommandBuilder to declaratively handle all command states:
// Instead of this:
ValueListenableBuilder<CommandResult<void, String>>(
valueListenable: command.results,
builder: (context, result, _) {
if (result.isRunning) return CircularProgressIndicator();
if (result.hasError) return Text('Error: ${result.error}');
return Text('Count: ${result.data}');
},
)
// Use this:
CommandBuilder(
command: command,
whileRunning: (context, _, __) => CircularProgressIndicator(),
onError: (context, error, _, __) => Text('Error: $error'),
onData: (context, value, _) => Text('Count: $value'),
)Benefits:
- Cleaner, more declarative code
- Separate builders for each state
- Less nesting than ValueListenableBuilder
- Type-safe parameter access
Basic Example
class CounterWidgetWithBuilder extends StatelessWidget {
const CounterWidgetWithBuilder({
super.key,
required this.manager,
});
final CounterManager manager;
@override
Widget build(BuildContext context) {
return Column(
children: [
CommandBuilder(
command: manager.incrementCommand,
whileRunning: (context, _, __) => CircularProgressIndicator(),
onData: (context, value, _) => Text('Count: $value'),
onError: (context, error, _, __) => Text('Error: $error'),
),
ElevatedButton(
onPressed: manager.incrementCommand.run,
child: Text('Increment'),
),
],
);
}
}Parameters
All parameters are optional except command:
Generic Types:
TParam- The parameter that was passed when the command was called (e.g., the search query)TResult- The return value from the command's execution
| Parameter | Type | Description |
|---|---|---|
| command | Command<TParam, TResult> | Required. The command to observe |
| onData | Widget Function(BuildContext, TResult, TParam?) | Builder for successful execution with return value |
| onSuccess | Widget Function(BuildContext, TParam?) | Builder for successful execution (ignores return value) |
| onNullData | Widget Function(BuildContext, TParam?) | Builder when command returns null |
| whileRunning | Widget Function(BuildContext, TResult?, TParam?) | Builder while command is executing |
| onError | Widget Function(BuildContext, Object, TResult?, TParam?) | Builder when command throws error |
| runCommandOnFirstBuild | bool | If true, executes command in initState (default: false) |
| initialParam | TParam? | Parameter to pass when runCommandOnFirstBuild is true |
When to Use Each Builder
onData - Commands with return values:
CommandBuilder(
command: searchCommand,
onData: (context, items, query) => ItemList(items), // ✅ Use items
)onSuccess - Void commands or when you don't need the result:
CommandBuilder(
command: deleteCommand,
onSuccess: (context, deletedItem) => Text('Deleted: ${deletedItem?.name}'),
)onNullData - Handle null results explicitly:
CommandBuilder(
command: fetchCommand,
onData: (context, data, _) => DataWidget(data),
onNullData: (context, _) => Text('No data available'),
)whileRunning - Show loading state:
whileRunning: (context, lastValue, param) => Column(
children: [
CircularProgressIndicator(),
if (lastValue != null) Text('Previous: $lastValue'), // Show stale data
if (param != null) Text('Loading: $param'),
],
)onError - Handle errors:
onError: (context, error, lastValue, param) => ErrorWidget(
error: error,
onRetry: () => command(param), // Retry with same parameter
)TIP
The lastValue parameter in whileRunning and onError will only contain data if the command was created with includeLastResultInCommandResults: true. Otherwise, it will always be null. See includeLastResultInCommandResults.
Showing Parameter in UI
Access the command parameter in any builder:
CommandBuilder(
command: searchCommand,
whileRunning: (context, _, query) => Text('Searching for: $query'),
onData: (context, items, query) => Column(
children: [
Text('Results for: $query'),
ItemList(items),
],
),
onError: (context, error, _, query) => Text('Search "$query" failed: $error'),
)Auto-Running Commands on Mount
CommandBuilder can automatically execute a command when the widget is first built using the runCommandOnFirstBuild parameter. This is particularly useful when not using watch_it (which provides callOnce for this purpose).
Basic Usage (No Parameter)
CommandBuilder(
command: loadTodosCommand,
runCommandOnFirstBuild: true, // Executes command in initState
whileRunning: (context, _, __) => CircularProgressIndicator(),
onData: (context, todos, _) => TodoList(todos),
onError: (context, error, _, __) => ErrorWidget(error),
)What happens:
- Widget builds
- Command executes automatically in
initState - UI shows loading state → data/error state
- Command only runs once - not on rebuilds
With Parameters
Use initialParam to pass a parameter to the command:
CommandBuilder(
command: searchCommand,
runCommandOnFirstBuild: true,
initialParam: 'flutter', // Parameter to pass
whileRunning: (context, _, query) => Text('Searching for: $query'),
onData: (context, items, query) => ItemList(items),
onError: (context, error, _, query) => Text('Search failed: $error'),
)When to Use
✅ Use runCommandOnFirstBuild when:
- ✅ Not using
watch_it(no access tocallOnce) - ✅ Widget should load its own data on mount
- ✅ Want self-contained data-loading widgets
- ✅ Simple data fetching scenarios
❌️ Don't use when:
- ❌️ Using
watch_it- prefercallOnceinstead (clearer separation) - ❌️
Commandis already running elsewhere - ❌️ Need conditional logic before running
Comparison with watch_it's callOnce
With watch_it (recommended if using watch_it):
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
callOnce((Manager m) => m.loadTodos()); // Explicit trigger
return CommandBuilder(
command: getIt<Manager>().loadTodos,
onData: (context, todos, _) => TodoList(todos),
);
}
}Without watch_it (use runCommandOnFirstBuild):
class TodoWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CommandBuilder(
command: getIt<Manager>().loadTodos,
runCommandOnFirstBuild: true, // Built-in trigger
onData: (context, todos, _) => TodoList(todos),
);
}
}Builder Precedence Rules
Both CommandBuilder and CommandResult.toWidget() use the same precedence rules when determining which builder to call:
Full precedence order:
if (error != null)→ callonErrorif (isRunning)→ callwhileRunningif (onSuccess != null)→ callonSuccess⚠️ Takes priority over onData!if (data != null)→ callonDataelse→ callonNullData
onData vs onSuccess
When command completes successfully:
- If
onSuccessprovided → call it (doesn't check if data is null) - Else if data != null → call
onData - Else → call
onNullData
Choose onSuccess when:
- Command returns void (e.g.,
Command.createAsyncNoResult) - You only need to show confirmation/success message
- Result data is irrelevant to the UI
Choose onData when:
- Command returns data you need to display/use
- You want to handle non-null data differently from null data
toWidget() Extension Method
The .toWidget() extension method on CommandResult provides the same declarative builder pattern as CommandBuilder, but for use when you already have access to a CommandResult (e.g., via watch_it, provider, or flutter_hooks).
class WeatherToWidgetExample extends WatchingWidget {
@override
Widget build(BuildContext context) {
final results = watchValue(
(WeatherManager m) => m.loadWeatherCommand.results,
);
return results.toWidget(
onData: (weather, param) {
return ListView.builder(
itemCount: weather.length,
itemBuilder: (context, index) {
final entry = weather[index];
return ListTile(
title: Text(entry.city),
subtitle: Text(entry.condition),
trailing: Text('${entry.temperature}°F'),
);
},
);
},
whileRunning: (lastWeather, param) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading weather for ${param ?? ""}...'),
],
),
);
},
onError: (error, lastWeather, param) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, color: Colors.red, size: 48),
SizedBox(height: 16),
Text('Error: $error'),
if (param != null) Text('For city: $param'),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => weatherManager.loadWeatherCommand('London'),
child: Text('Retry'),
),
],
),
);
},
);
}
}Parameters:
You must provide at least one of these two:
onData-Widget Function(TResult result, TParam? param)?- Called when command has non-null data (only if
onSuccessnot provided) - Receives both the result data and parameter
- Use for commands that return data you need to display
- Called when command has non-null data (only if
onSuccess-Widget Function(TParam? param)?- Called on successful completion (no error, not running)
- Does NOT receive result data, only the parameter
- Takes priority over
onDataif both provided - Use for void-returning commands or when you don't need the result value
Optional builders:
whileRunning-Widget Function(TResult? lastResult, TParam? param)?- Called while command executes
- Receives last result (if
includeLastResultInCommandResults: true) and parameter
onError-Widget Function(Object error, TResult? lastResult, TParam? param)?- Called when error occurs
- Receives error, last result, and parameter
onNullData-Widget Function(TParam? param)?- Called when data is null (only if neither
onSuccessnoronDatahandle it) - Receives only the parameter
- Called when data is null (only if neither
Key differences from CommandBuilder:
| Feature | CommandBuilder | toWidget() |
|---|---|---|
| BuildContext in builders | ✅ Yes (as parameter) | ❌️ No (access from enclosing build) |
| Requires CommandResult | ❌️ No (takes Command) | ✅ Yes |
| Use case | Direct Command usage | Already watching results |
| Builder precedence | Same as toWidget() | Same as CommandBuilder |
When to Use What
Use CommandBuilder when:
- Building UI directly from a Command
- Prefer declarative widget composition
- Don't use state management that exposes results
- Want BuildContext passed to builder functions
Use toWidget() when:
- Already watching
command.resultsvia watch_it/provider/hooks - Want simpler builder signatures (no BuildContext parameter)
- Prefer less boilerplate when already subscribed to results
Use ValueListenableBuilder when:
- Need complete control over rendering logic
- Complex state combinations beyond standard patterns
- Performance-critical custom caching logic
Common Patterns
Loading with Previous Data
Show stale data while loading fresh data:
Required Configuration
This pattern requires the command to be created with includeLastResultInCommandResults: true. Without this option, lastItems will always be null during execution. See Command Results - includeLastResultInCommandResults for details.
// Command must be created with this option:
final searchCommand = Command.createAsync<String, List<Item>>(
searchApi,
[],
includeLastResultInCommandResults: true, // Required for pattern below
);
CommandBuilder(
command: searchCommand,
whileRunning: (context, lastItems, query) => Column(
children: [
LinearProgressIndicator(),
if (lastItems != null)
Opacity(opacity: 0.5, child: ItemList(lastItems)),
],
),
onData: (context, items, _) => ItemList(items),
)Error with Retry
Required Configuration
To display the last successful value (line 7), the command must be created with includeLastResultInCommandResults: true. See Command Results - includeLastResultInCommandResults.
onError: (context, error, lastValue, param) => Column(
children: [
Text('Error: $error'),
ElevatedButton(
onPressed: () => command(param), // Retry with same parameter
child: Text('Retry'),
),
if (lastValue != null) Text('Last successful: $lastValue'),
],
)Conditional Builders
Not all builders are required - only provide what you need:
// Minimal: only show data
CommandBuilder(
command: command,
onData: (context, data, _) => Text(data),
)
// No loading indicator needed
CommandBuilder(
command: command,
onData: (context, data, _) => Text(data),
onError: (context, error, _, __) => Text('Error: $error'),
// whileRunning omitted - shows nothing while loading
)See Also
- Command Results - Understanding CommandResult structure
- Command Basics - Creating and running commands
- Command Properties - The
.resultsproperty - Observing Commands with watch_it - Using with reactive state management