Integration with watch_it
AI-Generated Content Under Review
This documentation was generated with AI assistance and is currently under review. While we strive for accuracy, there may be errors or inconsistencies. Please report any issues you find.
Use commands with watch_it for builder-free reactive UI. Observe command state without ValueListenableBuilder widgets.
Overview
watch_it provides a cleaner alternative to ValueListenableBuilder for observing commands:
Without watch_it:
ValueListenableBuilder<List<Todo>>(
valueListenable: command,
builder: (context, todos, _) {
return ValueListenableBuilder<bool>(
valueListenable: command.isRunning,
builder: (context, isRunning, _) {
// Nested builders...
},
);
},
)With watch_it:
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final todos = watchValue((Service s) => s.command);
final isRunning = watchValue((Service s) => s.command.isRunning);
// Direct usage, no builders!
}
}Basic Integration
class TodoService {
final api = ApiClient();
late final loadTodosCommand = Command.createAsyncNoParam<List<Todo>>(
() => api.fetchTodos(),
initialValue: [],
);
late final addTodoCommand = Command.createAsyncNoResult<Todo>(
(todo) async {
await simulateDelay();
// After adding, reload the list
loadTodosCommand.run();
},
);
}
// Register with get_it
void setupDependencies() {
GetIt.instance.registerLazySingleton<TodoService>(() => TodoService());
}
// Using watch_it to observe commands without builders
class TodoListWidget extends WatchingWidget {
const TodoListWidget({super.key});
@override
Widget build(BuildContext context) {
// Watch the command result directly
final todos = watchValue((TodoService s) => s.loadTodosCommand);
// Watch loading state
final isLoading =
watchValue((TodoService s) => s.loadTodosCommand.isRunning);
// Watch canRun for the add command
final canAdd = watchValue((TodoService s) => s.addTodoCommand.canRun);
final service = GetIt.instance<TodoService>();
return Scaffold(
appBar: AppBar(title: Text('Todos')),
body: Column(
children: [
if (isLoading) LinearProgressIndicator() else SizedBox(height: 4),
Expanded(
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo.title),
leading: Checkbox(
value: todo.completed,
onChanged: null,
),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: canAdd
? () => service
.addTodoCommand(Todo('${todos.length + 1}', 'New Todo', false))
: null,
child: Icon(Icons.add),
),
);
}
}
// Alternative: Watch CommandResult for comprehensive state
class TodoListWithResultWidget extends WatchingWidget {
const TodoListWithResultWidget({super.key});
@override
Widget build(BuildContext context) {
// Watch the complete result object
final result = watchValue(
(TodoService s) => s.loadTodosCommand.results,
);
final service = GetIt.instance<TodoService>();
return Scaffold(
appBar: AppBar(title: Text('Todos')),
body: () {
// Use result to handle all states
if (result.isRunning) {
return Center(child: CircularProgressIndicator());
}
if (result.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, color: Colors.red, size: 48),
SizedBox(height: 16),
Text('Error: ${result.error}'),
SizedBox(height: 16),
ElevatedButton(
onPressed: service.loadTodosCommand.run,
child: Text('Retry'),
),
],
),
);
}
if (result.hasData && result.data!.isNotEmpty) {
return ListView.builder(
itemCount: result.data!.length,
itemBuilder: (context, index) {
final todo = result.data![index];
return ListTile(
title: Text(todo.title),
leading: Checkbox(
value: todo.completed,
onChanged: null,
),
);
},
);
}
return Center(child: Text('No todos'));
}(),
);
}
}How it works:
- Register command-containing service with get_it
- Extend
WatchingWidget(orWatchingStatefulWidget) - Use
watchValue()to observe command properties - Widget rebuilds automatically when values change
watchValue with Commands
Watch Command Result
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Watch the command's value (last successful result)
final data = watchValue((DataService s) => s.loadCommand);
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) => ItemTile(data[index]),
);
}
}Watch Loading State
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final isLoading = watchValue((DataService s) => s.loadCommand.isRunning);
if (isLoading) {
return CircularProgressIndicator();
}
return DataView();
}
}Watch canRun
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final canSave = watchValue((DataService s) => s.saveCommand.canRun);
final service = GetIt.instance<DataService>();
return ElevatedButton(
onPressed: canSave ? service.saveCommand.run : null,
child: Text('Save'),
);
}
}Watch CommandResult
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Watch comprehensive state
final result = watchValue(
(DataService s) => s.loadCommand.results,
);
if (result.isRunning) return LoadingView();
if (result.hasError) return ErrorView(result.error!);
if (result.hasData) return DataView(result.data!);
return InitialView();
}
}Multiple Command Properties
Watch multiple properties in the same widget:
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Watch multiple command properties
final todos = watchValue((TodoService s) => s.loadTodosCommand);
final isLoading = watchValue((TodoService s) => s.loadTodosCommand.isRunning);
final canAdd = watchValue((TodoService s) => s.addTodoCommand.canRun);
final hasError = watchValue((TodoService s) => s.loadTodosCommand.errors);
final service = GetIt.instance<TodoService>();
return Column(
children: [
if (isLoading) LinearProgressIndicator(),
if (hasError != null) ErrorBanner(error: hasError.error),
Expanded(
child: TodoList(todos: todos),
),
FloatingActionButton(
onPressed: canAdd ? () => service.addTodoCommand(newTodo) : null,
child: Icon(Icons.add),
),
],
);
}
}Each watchValue creates an independent subscription - the widget rebuilds when any watched value changes.
Error Handling with watch_it
class DataWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final result = watchValue((DataService s) => s.loadCommand.results);
final service = GetIt.instance<DataService>();
return Column(
children: [
if (result.hasError)
Card(
color: Colors.red.shade50,
child: ListTile(
leading: Icon(Icons.error, color: Colors.red),
title: Text('Error: ${result.error}'),
trailing: IconButton(
icon: Icon(Icons.refresh),
onPressed: service.loadCommand.run,
),
),
),
if (result.hasData)
DataDisplay(data: result.data!),
],
);
}
}Service with Commands Pattern
Recommended pattern: Commands in services, observed via watch_it:
// Service containing commands
class UserService {
final api = ApiClient();
late final loginCommand = Command.createAsync<LoginData, User>(
(data) => api.login(data.email, data.password),
initialValue: User.empty(),
);
late final logoutCommand = Command.createAsyncNoParam<void>(
() => api.logout(),
);
late final loadProfileCommand = Command.createAsyncNoParam<UserProfile>(
() => api.loadProfile(),
initialValue: UserProfile.empty(),
restriction: loginCommand.map((user) => !user.isLoggedIn),
);
void dispose() {
loginCommand.dispose();
logoutCommand.dispose();
loadProfileCommand.dispose();
}
}
// Register with get_it
void setupServices() {
GetIt.instance.registerLazySingleton<UserService>(() => UserService());
}
// UI observes via watch_it
class ProfileWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final profile = watchValue((UserService s) => s.loadProfileCommand);
final isLoading = watchValue((UserService s) => s.loadProfileCommand.isRunning);
if (isLoading) return CircularProgressIndicator();
return ProfileView(profile: profile);
}
}WatchingWidget vs WatchingStatefulWidget
Use WatchingWidget for stateless observation
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final data = watchValue((Service s) => s.command);
return DataView(data: data);
}
}Use WatchingStatefulWidget when you need local state
class MyWidget extends WatchingStatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool showDetails = false; // Local state
@override
Widget build(BuildContext context) {
// Can watch commands AND use local state
final data = watchValue((Service s) => s.command);
return Column(
children: [
DataView(data: data, showDetails: showDetails),
ElevatedButton(
onPressed: () => setState(() => showDetails = !showDetails),
child: Text('Toggle Details'),
),
],
);
}
}Reacting to Command Execution
Use registerHandler to run code when commands complete:
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final service = GetIt.instance<TodoService>();
// React to command completion
registerHandler(
select: (TodoService s) => s.addTodoCommand.results,
handler: (context, result, cancel) {
if (result.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Todo added!')),
);
}
},
);
final todos = watchValue((TodoService s) => s.loadTodosCommand);
return TodoList(
todos: todos,
onAdd: service.addTodoCommand,
);
}
}Combining Commands with watch_it Lifecycle
callOnce for Initial Load
class DataWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Load data once when widget mounts
callOnce((DataService s) => s.loadCommand.run);
final data = watchValue((DataService s) => s.loadCommand);
final isLoading = watchValue((DataService s) => s.loadCommand.isRunning);
if (isLoading) return CircularProgressIndicator();
return DataView(data: data);
}
}rebuildOnChange for Manual Control
class DataWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Only rebuild when data changes, not when isRunning changes
final data = rebuildOnChange((DataService s) => s.loadCommand.value);
return DataView(data: data);
}
}Performance Considerations
Watch Specific Properties
// ❌️️ Inefficient: Rebuilds on every command property change
final command = watchValue((Service s) => s.command);
// ✅ Efficient: Only rebuilds when result value changes
final data = watchValue((Service s) => s.command.value);Use rebuildOnChange for Selective Rebuilds
// Only rebuild when canRun changes
final canRun = rebuildOnChange((Service s) => s.command.canRun.value);
// Not when errors change
final data = watchValue((Service s) => s.command);Avoid Watching in Nested Widgets
// ❌️️ Bad: Multiple widgets watching same command
class ParentWidget extends WatchingWidget {
Widget build(BuildContext context) {
final data = watchValue((Service s) => s.command);
return ChildWidget(); // Also watches command
}
}
// ✅ Good: Watch once at parent, pass down
class ParentWidget extends WatchingWidget {
Widget build(BuildContext context) {
final data = watchValue((Service s) => s.command);
return ChildWidget(data: data); // Receives data as prop
}
}Common Patterns
Pattern 1: Pull-to-Refresh
class DataWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final data = watchValue((DataService s) => s.loadCommand);
final service = GetIt.instance<DataService>();
return RefreshIndicator(
onRefresh: () => service.loadCommand.runAsync(),
child: ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) => ItemTile(data[index]),
),
);
}
}Pattern 2: Search with Debounce
class SearchWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final results = watchValue((SearchService s) => s.searchCommand);
final isSearching = watchValue((SearchService s) => s.searchCommand.isRunning);
final service = GetIt.instance<SearchService>();
return Column(
children: [
TextField(
onChanged: service.searchTextCommand,
decoration: InputDecoration(
hintText: 'Search...',
suffixIcon: isSearching ? CircularProgressIndicator() : null,
),
),
Expanded(
child: SearchResults(results: results),
),
],
);
}
}Pattern 3: Chained Commands
class CheckoutWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final service = GetIt.instance<CheckoutService>();
// Watch multiple chained commands
final canValidate = watchValue((CheckoutService s) => s.validateCommand.canRun);
final canSubmit = watchValue((CheckoutService s) => s.submitCommand.canRun);
final isProcessing = watchValue((CheckoutService s) => s.submitCommand.isRunning);
return Column(
children: [
CheckoutForm(),
ElevatedButton(
onPressed: canValidate ? service.validateCommand.run : null,
child: Text('Validate'),
),
ElevatedButton(
onPressed: canSubmit ? service.submitCommand.run : null,
child: Text(isProcessing ? 'Processing...' : 'Submit'),
),
],
);
}
}Comparison: ValueListenableBuilder vs watch_it
ValueListenableBuilder Approach
class TodoWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final service = GetIt.instance<TodoService>();
return ValueListenableBuilder<bool>(
valueListenable: service.loadCommand.isRunning,
builder: (context, isLoading, _) {
return ValueListenableBuilder<List<Todo>>(
valueListenable: service.loadCommand,
builder: (context, todos, _) {
return ValueListenableBuilder<bool>(
valueListenable: service.addCommand.canRun,
builder: (context, canAdd, _) {
// Deeply nested builders
if (isLoading) return CircularProgressIndicator();
return TodoListView(todos: todos, canAdd: canAdd);
},
);
},
);
},
);
}
}watch_it Approach
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final isLoading = watchValue((TodoService s) => s.loadCommand.isRunning);
final todos = watchValue((TodoService s) => s.loadCommand);
final canAdd = watchValue((TodoService s) => s.addCommand.canRun);
if (isLoading) return CircularProgressIndicator();
return TodoListView(todos: todos, canAdd: canAdd);
}
}Benefits:
- No nested builders
- Flat, readable code
- Multiple observations without nesting
- Same rebuild efficiency
See Also
- Command Properties — Observable properties
- Command Basics — Creating commands
- watch_it Documentation — watch_it package
- Best Practices — Recommended patterns