Using Commands without watch_it
All the examples in Getting Started use watch_it, which is our recommended approach for production apps. However, commands work perfectly with plain ValueListenableBuilder if you prefer not to use watch_it or get_it.
When to Use ValueListenableBuilder
Consider using ValueListenableBuilder instead of watch_it when:
- You're prototyping or learning and want to minimize dependencies
- You have a simple widget that doesn't need dependency injection
- You prefer explicit builder patterns over implicit observation
- You're working on a project that doesn't use get_it
For production apps, we still recommend watch_it for cleaner, more maintainable code.
Simple Counter Example
Here's the basic counter example using ValueListenableBuilder:
class CounterModel {
int _count = 0;
// Command wraps a function and acts as a ValueListenable
late final incrementCommand = Command.createSyncNoParam<String>(
() {
_count++;
return _count.toString();
},
initialValue: '0',
);
}
class CounterWidget extends StatelessWidget {
CounterWidget({super.key});
final model = CounterModel();
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You have pushed the button this many times:'),
// Command is a ValueListenable - use ValueListenableBuilder
ValueListenableBuilder<String>(
valueListenable: model.incrementCommand,
builder: (context, value, _) => Text(
value,
style: Theme.of(context).textTheme.headlineMedium,
),
),
SizedBox(height: 16),
// Command has a .run method - use it as tearoff for onPressed
ElevatedButton(
onPressed: model.incrementCommand.run,
child: Text('Increment'),
),
],
);
}
}Key points:
- Use
ValueListenableBuilderto observe the command - Use
StatelessWidgetinstead ofWatchingWidget - No need for get_it registration - service can be created directly in the widget
- Command is still a
ValueListenable, just observed differently
Async Example with Loading States
Here's the weather example showing async commands with loading indicators:
class WeatherWidget extends StatelessWidget {
WeatherWidget({super.key});
final manager = WeatherManager();
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: manager.loadWeatherCommand.isRunning,
builder: (context, isRunning, _) {
// Show loading indicator while command runs
if (isRunning) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading weather data...'),
],
),
);
}
// Show data when ready
return ValueListenableBuilder<List<WeatherEntry>>(
valueListenable: manager.loadWeatherCommand,
builder: (context, weather, _) {
if (weather.isEmpty) {
return Center(
child: ElevatedButton(
onPressed: () => manager.loadWeatherCommand('London'),
child: Text('Load Weather'),
),
);
}
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'),
);
},
);
},
);
},
);
}
}Key points:
- Watch
isRunningwith a separateValueListenableBuilderfor loading state - Nested builders required - one for loading state, one for data
- More verbose than watch_it but works without additional dependencies
- All command features (async, error handling, restrictions) still work
Comparing the Approaches
| Aspect | watch_it | ValueListenableBuilder |
|---|---|---|
| Dependencies | Requires get_it + watch_it | No additional dependencies |
| Widget Base | WatchingWidget | StatelessWidget or StatefulWidget |
| Observation | watchValue((Service s) => s.command) | ValueListenableBuilder(valueListenable: command, ...) |
| Multiple Properties | Clean - separate watchValue calls | Nested builders required |
| Boilerplate | Minimal | More verbose |
| Recommended For | Production apps | Learning, prototyping |
Observing Multiple Properties
When you need to observe both the command result AND its state (like isRunning), the difference becomes more apparent:
With watch_it (Recommended)
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final data = watchValue((Service s) => s.command);
final isLoading = watchValue((Service s) => s.command.isRunning);
if (isLoading) return CircularProgressIndicator();
return Text(data);
}
}With ValueListenableBuilder
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: command.isRunning,
builder: (context, isLoading, _) {
if (isLoading) return CircularProgressIndicator();
return ValueListenableBuilder<String>(
valueListenable: command,
builder: (context, data, _) {
return Text(data);
},
);
},
);
}
}Notice the nesting required with ValueListenableBuilder versus the clean, flat structure with watch_it.
Using CommandResult
For the cleanest ValueListenableBuilder experience, use CommandResult to observe all command state in a single builder:
class MyWidget extends StatelessWidget {
final myCommand = Command.createAsync<void, String>(
() async => 'Hello',
initialValue: '',
);
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<CommandResult<void, String>>(
valueListenable: myCommand.results,
builder: (context, result, _) {
if (result.isRunning) {
return CircularProgressIndicator();
}
if (result.hasError) {
return Text('Error: ${result.error}');
}
return Text(result.data);
},
);
}
}See Command Results for more details on using CommandResult.
Error Handling
Commands notify errors through the errors property. Here's how to handle them without watch_it:
class DataWidget extends StatefulWidget {
const DataWidget({super.key});
@override
State<DataWidget> createState() => _DataWidgetState();
}
class _DataWidgetState extends State<DataWidget> {
final manager = DataManager();
String? errorMessage;
@override
void initState() {
super.initState();
// Listen to errors
manager.loadDataCommand.errors.listen((error, _) {
if (error != null) {
setState(() {
errorMessage = error.error.toString();
});
// Show error dialog
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text(error.error.toString()),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
} else {
// Error cleared (command started again)
setState(() {
errorMessage = null;
});
}
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
if (errorMessage != null)
Padding(
padding: EdgeInsets.all(8),
child: Text(
errorMessage!,
style: TextStyle(color: Colors.red),
),
),
ElevatedButton(
onPressed: manager.loadDataCommand.run,
child: Text('Load Data'),
),
SizedBox(height: 8),
ElevatedButton(
onPressed: () {
manager.shouldFail = true;
manager.loadDataCommand.run();
},
child: Text('Load Data (will fail)'),
),
],
);
}
}Filtering null values:
The errors property is set to null at the start of execution (without notification). To only handle actual errors:
command.errors.where((e) => e != null).listen((error, _) {
// Only called for actual errors, not null clears
showErrorDialog(error!.error.toString());
});When to set up listeners:
- In
initStateof aStatefulWidget - Using
registerHandlerfrom listen_it - Prefer these over watching in
build()to avoid recreating listeners
For watch_it error handling patterns, see Command Properties - Error Notifications.
Next Steps
Ready to learn more?
- Want to use watch_it? See watch_it Integration for comprehensive patterns
- Need more command features? Check out Command Properties, Error Handling, and Restrictions
- Building production apps? Read Best Practices for architecture guidance
For more about watch_it and why we recommend it, see the watch_it documentation.