Command Restrictions
Control when commands can execute using reactive conditions. Restrictions enable declarative connection of command behavior - connect commands to application state or to each other, and they automatically enable/disable based on those conditions.
Key benefits:
- Reactive coordination - Commands respond to state changes automatically
- Declarative dependencies - Chain commands together without manual orchestration
- Automatic UI updates -
canRunreflects restrictions instantly - Centralized logic - No scattered
ifchecks throughout your code
Overview
Commands can be conditionally enabled or disabled using the restriction parameter, which accepts a ValueListenable<bool>. This allows restrictions to change dynamically after the command is created - the command automatically responds to state changes.
Key concept: When the restriction's current value is true, the command is disabled
Command.createAsyncNoParam<List<Todo>>(
() => api.fetchTodos(),
initialValue: [],
restriction: isLoggedIn.map((logged) => !logged), // disabled when NOT logged in
);Any change of restriction is reflected in the canRun property of the command with this formula: canRun = !isRunning && !restriction
UI Integration
Because canRun automatically reflects both execution state and restrictions, it's ideal for enabling/disabling UI elements. Just watch canRun and your buttons automatically enable/disable as conditions change - no manual state tracking needed.
Basic Restriction with ValueNotifier
The most common pattern is restricting based on application state:
class DataWidget extends StatelessWidget {
final manager = DataManager();
DataWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Observe login state
ValueListenableBuilder<bool>(
valueListenable: manager.isLoggedIn,
builder: (context, isLoggedIn, _) {
return Column(
children: [
Text(
isLoggedIn ? 'Logged In' : 'Not Logged In',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => manager.isLoggedIn.value = !isLoggedIn,
child: Text(isLoggedIn ? 'Log Out' : 'Log In'),
),
],
);
},
),
const SizedBox(height: 16),
// Observe canRun - automatically reflects restriction + isRunning
ValueListenableBuilder<bool>(
valueListenable: manager.loadDataCommand.canRun,
builder: (context, canRun, _) {
return ElevatedButton(
onPressed: canRun ? manager.loadDataCommand.run : null,
child: const Text('Load Data'),
);
},
),
const SizedBox(height: 16),
// Observe results
ValueListenableBuilder<CommandResult<void, List<Todo>>>(
valueListenable: manager.loadDataCommand.results,
builder: (context, result, _) {
if (result.isRunning) {
return const CircularProgressIndicator();
}
if (result.hasError) {
return Text(
'Error: ${result.error}',
style: const TextStyle(color: Colors.red),
);
}
if (result.data?.isEmpty ?? true) {
return const Text('No data loaded');
}
return Text('Loaded ${result.data?.length ?? 0} todos');
},
),
],
);
}
}How it works:
- Create a
ValueNotifier<bool>to track state (isLoggedIn) - Map it to restriction logic:
!loggedmeans "restrict when NOT logged in" - Command automatically updates
canRunproperty - Use
watchValue()to observecanRunin your widget - Button automatically disables when
canRunis false
Important: The restriction parameter expects ValueListenable<bool> where true means "disabled". Because it's a ValueListenable, the restriction can change at any time - the command automatically reacts and updates canRun accordingly.
Chaining Commands via isRunningSync
Prevent commands from running while other commands execute:
class DataManager {
final api = ApiClient();
// First command: load initial data
late final loadCommand = Command.createAsyncNoParam<List<Todo>>(
() => api.fetchTodos(),
initialValue: [],
);
// Second command: can't save while loading
late final saveCommand = Command.createAsyncNoResult<Todo>(
(todo) async {
await simulateDelay();
// Save logic here
},
// Restrict based on first command's running state
restriction: loadCommand.isRunningSync,
);
// Third command: can't update while loading OR saving
late final updateCommand = Command.createAsyncNoResult<Todo>(
(todo) async {
await simulateDelay(500);
// Update logic here
},
// Combine multiple restrictions: disabled if EITHER command is running
restriction: loadCommand.isRunningSync.combineLatest(
saveCommand.isRunningSync,
(isLoading, isSaving) => isLoading || isSaving,
),
);
}
class ChainedCommandsWidget extends WatchingWidget {
const ChainedCommandsWidget({super.key});
@override
Widget build(BuildContext context) {
// Watch all canRun states
final canRunLoad = watchValue((DataManager m) => m.loadCommand.canRun);
final canRunSave = watchValue((DataManager m) => m.saveCommand.canRun);
final canRunUpdate = watchValue((DataManager m) => m.updateCommand.canRun);
final todos = watchValue((DataManager m) => m.loadCommand);
return Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Command Chaining Example',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 16),
// Load button
ElevatedButton(
onPressed: canRunLoad ? di<DataManager>().loadCommand.run : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!canRunLoad) ...[
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
),
SizedBox(width: 8),
],
Text('Load Data'),
],
),
),
SizedBox(height: 8),
// Save button - disabled while loading
ElevatedButton(
onPressed: canRunSave
? () =>
di<DataManager>().saveCommand(Todo('1', 'Test Todo', false))
: null,
child:
Text(canRunSave ? 'Save Todo' : 'Save (blocked while loading)'),
),
SizedBox(height: 8),
// Update button - disabled while loading OR saving
ElevatedButton(
onPressed: canRunUpdate
? () => di<DataManager>()
.updateCommand(Todo('2', 'Updated Todo', false))
: null,
child: Text(canRunUpdate
? 'Update Todo'
: 'Update (blocked while loading/saving)'),
),
SizedBox(height: 16),
// Status display
Text('Loaded ${todos.length} todos'),
],
),
);
}
}How it works:
saveCommandusesloadCommand.isRunningSyncas restriction- While loading,
saveCommandcannot run updateCommandusescombineLatestto combine both running states- Update is disabled if EITHER load OR save is running
- Demonstrates combining multiple restrictions with listen_it operators
Why isRunningSync?
isRunningupdates asynchronously to avoid race conditions in UI rebuildingisRunningSyncupdates immediately- Prevents race conditions in restrictions
- Use
isRunningfor UI,isRunningSyncfor restrictions
canRun Property
canRun automatically combines running state and restrictions:
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final canRun = watchValue((MyManager m) => m.command.canRun);
return ElevatedButton(
onPressed: canRun ? di<MyManager>().command.run : null,
child: Text('Execute'),
);
}
}canRun is true when:
- Command is NOT running (
!isRunning) - AND restriction is false (
!restriction)
This is more convenient than manually checking both conditions.
Restriction Patterns
Authentication-Based Restriction
final isAuthenticated = ValueNotifier<bool>(false);
late final dataCommand = Command.createAsyncNoParam<Data>(
() => api.fetchSecureData(),
initialValue: Data.empty(),
restriction: isAuthenticated.map((auth) => !auth), // disabled when not authenticated
);Validation-Based Restriction
final formValid = ValueNotifier<bool>(false);
late final submitCommand = Command.createAsync<FormData, void>(
(data) => api.submit(data),
restriction: formValid.map((valid) => !valid), // disabled when invalid
);Multiple Conditions
Use ValueListenable operators to combine restrictions:
final isOnline = ValueNotifier<bool>(true);
final hasPermission = ValueNotifier<bool>(false);
late final syncCommand = Command.createAsyncNoParam<void>(
() => api.sync(),
// Disabled when offline OR no permission
restriction: isOnline.combineLatest(
hasPermission,
(online, permission) => !online || !permission,
),
);Temporary Restrictions
Restrict commands during specific operations:
class DataManager {
final isSyncing = ValueNotifier<bool>(false);
late final deleteCommand = Command.createAsync<String, void>(
(id) => api.delete(id),
// Can't delete while syncing
restriction: isSyncing,
);
Future<void> sync() async {
isSyncing.value = true;
try {
await api.syncAll();
} finally {
isSyncing.value = false;
}
}
}Even More Elegant
If you implement sync() as a command too, you can use its isRunningSync directly as the restriction - no need to manually manage isSyncing. See the Chaining Commands example above.
Alternative Actions with ifRestrictedRunInstead
When a command is restricted, you may want to take an alternative action instead of silently doing nothing. The ifRestrictedRunInstead parameter provides a fallback handler that executes when the command is restricted.
Common use cases:
- ✅ Show login dialog when user needs authentication
- ✅ Display error messages explaining why action can't be performed
- ✅ Log analytics events for restricted attempts
- ✅ Navigate to a different screen or show a modal
class DataService {
final isAuthenticated = ValueNotifier<bool>(false);
late final fetchDataCommand = Command.createAsync<String, List<String>>(
(query) async {
// Fetch data from API
final api = getIt<ApiClient>();
return await api.searchData(query);
},
initialValue: [], // initial value
restriction:
isAuthenticated.map((auth) => !auth), // disabled when not authenticated
ifRestrictedRunInstead: (query) {
// Called when command is restricted (not authenticated)
// Show login prompt instead of executing
debugPrint('Please log in to search for: $query');
showLoginDialog();
},
);
void showLoginDialog() {
// In a real app, this would show a dialog
debugPrint('Showing login dialog...');
}
}How it works:
- The handler receives the parameter that was passed to the command
- Called only when
restrictionistrue(command is disabled) - The original wrapped function is NOT executed
- Use it for user feedback or alternative flows
Access to Parameters
The ifRestrictedRunInstead handler receives the same parameter that would have been passed to the wrapped function. This allows you to provide context-aware feedback (e.g., "Please log in to search for '{query}'").
NoParam commands: For NoParam commands (createAsyncNoParam, createSyncNoParam), the ifRestrictedRunInstead handler has no parameter: void Function() instead of RunInsteadHandler<TParam>.
Restriction vs Manual Checks
❌️ Without restrictions (manual checks):
void handleSave() {
if (!isLoggedIn.value) return; // Manual check
if (command.isRunning.value) return; // Manual check
command.run();
}✅ With restrictions (automatic):
late final command = Command.createAsync<Data, void>(
(data) => api.save(data),
restriction: isLoggedIn.map((logged) => !logged),
);
// UI automatically disables when restricted
class SaveWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final canRun = watchValue((MyManager m) => m.command.canRun);
return ElevatedButton(
onPressed: canRun ? () => di<MyManager>().command(data) : null,
child: Text('Save'),
);
}
}Benefits:
- UI automatically reflects state
- No manual checks needed
- Centralized logic
- Reactive to state changes
Common Mistakes
❌️ Inverting the restriction logic
// WRONG: restriction expects true = disabled
restriction: isLoggedIn, // disabled when logged in (backwards!)// CORRECT: negate the condition
restriction: isLoggedIn.map((logged) => !logged), // disabled when NOT logged in❌️ Using isRunning for restrictions
// WRONG: async update can cause race conditions
restriction: otherCommand.isRunning,// CORRECT: use synchronous version
restriction: otherCommand.isRunningSync,❌️ Forgetting to dispose restriction sources
class Manager {
final customRestriction = ValueNotifier<bool>(false);
late final command = Command.createAsync<Data, void>(
(data) => api.save(data),
restriction: customRestriction,
);
void dispose() {
command.dispose();
customRestriction.dispose(); // Don't forget this!
}
}See Also
- Command Basics — Creating and running commands
- Command Properties — canRun, isRunning, isRunningSync
- Error Handling — Handling runtime errors
- listen_it Operators — ValueListenable operators