Command Restrictions
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.
Control when commands can execute using reactive conditions. Restrictions integrate with canRun to automatically disable commands based on application state.
Overview
Commands can be conditionally enabled or disabled using the restriction parameter. When a restriction is active (evaluates to true), the command cannot run.
Key concept: restriction: true = command is disabled
Command.createAsyncNoParam<List<Todo>>(
() => api.fetchTodos(),
initialValue: [],
restriction: isLoggedIn.map((logged) => !logged), // disabled when NOT logged in
);Formula: canRun = !isRunning && !restriction
Basic Restriction with ValueNotifier
The most common pattern is restricting based on application state:
class AuthManager {
// Control whether commands can run
final isLoggedIn = ValueNotifier<bool>(false);
final api = ApiClient();
late final loadDataCommand = Command.createAsyncNoParam<List<Todo>>(
() => api.fetchTodos(),
initialValue: [],
// Restrict when NOT logged in (restriction: true = disabled)
restriction: isLoggedIn.map((loggedIn) => !loggedIn),
);
void login() {
isLoggedIn.value = true;
}
void logout() {
isLoggedIn.value = false;
}
}
class RestrictedWidget extends StatelessWidget {
RestrictedWidget({super.key});
final manager = AuthManager();
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Show login status
ValueListenableBuilder<bool>(
valueListenable: manager.isLoggedIn,
builder: (context, isLoggedIn, _) {
return Text(
isLoggedIn ? 'Logged In' : 'Not Logged In',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isLoggedIn ? Colors.green : Colors.red,
),
);
},
),
SizedBox(height: 16),
// Login/Logout buttons
ValueListenableBuilder<bool>(
valueListenable: manager.isLoggedIn,
builder: (context, isLoggedIn, _) {
return ElevatedButton(
onPressed: isLoggedIn ? manager.logout : manager.login,
child: Text(isLoggedIn ? 'Logout' : 'Login'),
);
},
),
SizedBox(height: 16),
// Load data button - disabled when not logged in
ValueListenableBuilder<bool>(
valueListenable: manager.loadDataCommand.canRun,
builder: (context, canRun, _) {
return ElevatedButton(
onPressed: canRun ? manager.loadDataCommand.run : null,
child: Text('Load Data'),
);
},
),
],
);
}
}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 - UI disables buttons when
canRunis false
Important: The restriction parameter expects ValueListenable<bool> where true means "disabled".
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 saving
late final updateCommand = Command.createAsyncNoResult<Todo>(
(todo) async {
await simulateDelay(500);
// Update logic here
},
// Can't update while save is running
restriction: saveCommand.isRunningSync,
);
}
class ChainedCommandsWidget extends StatelessWidget {
ChainedCommandsWidget({super.key});
final manager = DataManager();
@override
Widget build(BuildContext context) {
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
ValueListenableBuilder<bool>(
valueListenable: manager.loadCommand.canRun,
builder: (context, canRun, _) {
return ElevatedButton(
onPressed: canRun ? manager.loadCommand.run : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!canRun) ...[
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
ValueListenableBuilder<bool>(
valueListenable: manager.saveCommand.canRun,
builder: (context, canRun, _) {
return ElevatedButton(
onPressed: canRun
? () => manager.saveCommand(Todo('1', 'Test Todo', false))
: null,
child:
Text(canRun ? 'Save Todo' : 'Save (blocked while loading)'),
);
},
),
SizedBox(height: 8),
// Update button - disabled while saving
ValueListenableBuilder<bool>(
valueListenable: manager.updateCommand.canRun,
builder: (context, canRun, _) {
return ElevatedButton(
onPressed: canRun
? () =>
manager.updateCommand(Todo('2', 'Updated Todo', false))
: null,
child: Text(
canRun ? 'Update Todo' : 'Update (blocked while saving)'),
);
},
),
SizedBox(height: 16),
// Status display
ValueListenableBuilder<List<Todo>>(
valueListenable: manager.loadCommand,
builder: (context, todos, _) {
return Text('Loaded ${todos.length} todos');
},
),
],
),
);
}
}How it works:
saveCommandusesloadCommand.isRunningSyncas restriction- While loading,
saveCommandcannot run updateCommandusessaveCommand.isRunningSync- Creates a dependency chain: load → save → update
Why isRunningSync?
isRunningupdates asynchronously (via microtask)isRunningSyncupdates immediately- Prevents race conditions in restrictions
- Use
isRunningfor UI,isRunningSyncfor restrictions
canRun Property
canRun automatically combines running state and restrictions:
ValueListenableBuilder<bool>(
valueListenable: command.canRun,
builder: (context, canRun, _) {
return ElevatedButton(
onPressed: canRun ? 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;
}
}
}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
ValueListenableBuilder<bool>(
valueListenable: command.canRun,
builder: (context, canRun, _) {
return ElevatedButton(
onPressed: canRun ? () => command(data) : null,
child: Text('Save'),
);
},
)Benefits:
- UI automatically reflects state
- No manual checks needed
- Centralized logic
- Reactive to state changes
Restrictions vs Error Handling
Restrictions prevent execution — the command never runs. Error handling deals with failures — the command runs but throws.
// Restriction: prevent execution when offline
restriction: isOnline.map((online) => !online)
// Error handling: handle failures when network fails during execution
errorFilter: PredicatesErrorFilter({
NetworkException: (error, _) => showRetryDialog(error),
})Use restrictions for known conditions (auth, validation, state). Use error handling for runtime failures (network, API errors).
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