Restricciones de Command
Controla cuándo los commands pueden ejecutarse usando condiciones reactivas. Las restricciones permiten conexión declarativa del comportamiento de commands - conecta commands al estado de la aplicación o entre sí, y automáticamente se habilitan/deshabilitan basándose en esas condiciones.
Beneficios clave:
- Coordinación reactiva - Los commands responden a cambios de estado automáticamente
- Dependencias declarativas - Encadena commands sin orquestación manual
- Actualizaciones de UI automáticas -
canRunrefleja restricciones instantáneamente - Lógica centralizada - Sin verificaciones
ifdispersas por tu código
Resumen
Los commands pueden ser condicionalmente habilitados o deshabilitados usando el parámetro restriction, que acepta un ValueListenable<bool>. Esto permite que las restricciones cambien dinámicamente después de que el command sea creado - el command automáticamente responde a cambios de estado.
Concepto clave: Cuando el valor actual de la restricción es true, el command está deshabilitado
Command.createAsyncNoParam<List<Todo>>(
() => api.fetchTodos(),
initialValue: [],
restriction: isLoggedIn.map((logged) => !logged), // deshabilitado cuando NO está logueado
);Cualquier cambio de restricción se refleja en la propiedad canRun del command con esta fórmula: canRun = !isRunning && !restriction
Integración de UI
Debido a que canRun automáticamente refleja tanto el estado de ejecución como las restricciones, es ideal para habilitar/deshabilitar elementos de UI. Solo observa canRun y tus botones automáticamente se habilitan/deshabilitan cuando las condiciones cambian - no se necesita tracking manual de estado.
Restricción Básica con ValueNotifier
El patrón más común es restringir basándose en estado de aplicación:
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');
},
),
],
);
}
}Cómo funciona:
- Crea un
ValueNotifier<bool>para trackear estado (isLoggedIn) - Mapéalo a lógica de restricción:
!loggedsignifica "restringir cuando NO está logueado" - El command automáticamente actualiza la propiedad
canRun - Usa
watchValue()para observarcanRunen tu widget - El botón automáticamente se deshabilita cuando
canRunes false
Importante: El parámetro restriction espera ValueListenable<bool> donde true significa "deshabilitado". Porque es un ValueListenable, la restricción puede cambiar en cualquier momento - el command automáticamente reacciona y actualiza canRun acordemente.
Encadenando Commands via isRunningSync
Prevén que commands se ejecuten mientras otros commands se ejecutan:
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'),
],
),
);
}
}Cómo funciona:
saveCommandusaloadCommand.isRunningSynccomo restricción- Mientras carga,
saveCommandno puede ejecutarse updateCommandusacombineLatestpara combinar ambos estados de ejecución- Update está deshabilitado si CUALQUIERA de load O save está ejecutándose
- Demuestra combinar múltiples restricciones con operadores de listen_it
¿Por qué isRunningSync?
isRunningse actualiza asíncronamente para evitar condiciones de carrera en rebuilding de UIisRunningSyncse actualiza inmediatamente- Previene condiciones de carrera en restricciones
- Usa
isRunningpara UI,isRunningSyncpara restricciones
Propiedad canRun
canRun automáticamente combina estado de ejecución y restricciones:
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('Ejecutar'),
);
}
}canRun es true cuando:
- El command NO está ejecutándose (
!isRunning) - Y la restricción es false (
!restriction)
Esto es más conveniente que verificar manualmente ambas condiciones.
Patrones de Restricción
Restricción Basada en Autenticación
final isAuthenticated = ValueNotifier<bool>(false);
late final dataCommand = Command.createAsyncNoParam<Data>(
() => api.fetchSecureData(),
initialValue: Data.empty(),
restriction: isAuthenticated.map((auth) => !auth), // deshabilitado cuando no autenticado
);Restricción Basada en Validación
final formValid = ValueNotifier<bool>(false);
late final submitCommand = Command.createAsync<FormData, void>(
(data) => api.submit(data),
restriction: formValid.map((valid) => !valid), // deshabilitado cuando inválido
);Múltiples Condiciones
Usa operadores de ValueListenable para combinar restricciones:
final isOnline = ValueNotifier<bool>(true);
final hasPermission = ValueNotifier<bool>(false);
late final syncCommand = Command.createAsyncNoParam<void>(
() => api.sync(),
// Deshabilitado cuando offline O sin permiso
restriction: isOnline.combineLatest(
hasPermission,
(online, permission) => !online || !permission,
),
);Restricciones Temporales
Restringe commands durante operaciones específicas:
class DataManager {
final isSyncing = ValueNotifier<bool>(false);
late final deleteCommand = Command.createAsync<String, void>(
(id) => api.delete(id),
// No puede eliminar mientras sincroniza
restriction: isSyncing,
);
Future<void> sync() async {
isSyncing.value = true;
try {
await api.syncAll();
} finally {
isSyncing.value = false;
}
}
}Aún Más Elegante
Si implementas sync() como un command también, puedes usar su isRunningSync directamente como restricción - no necesitas gestionar isSyncing manualmente. Ver el ejemplo de Encadenando Commands arriba.
Acciones Alternativas con ifRestrictedRunInstead
Cuando un command está restringido, podrías querer tomar una acción alternativa en lugar de silenciosamente no hacer nada. El parámetro ifRestrictedRunInstead proporciona un handler de fallback que se ejecuta cuando el command está restringido.
Casos de uso comunes:
- ✅ Mostrar diálogo de login cuando el usuario necesita autenticación
- ✅ Mostrar mensajes de error explicando por qué la acción no puede realizarse
- ✅ Loguear eventos de analytics para intentos restringidos
- ✅ Navegar a una pantalla diferente o mostrar un 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...');
}
}Cómo funciona:
- El handler recibe el parámetro que fue pasado al command
- Se llama solo cuando
restrictionestrue(command está deshabilitado) - La función envuelta original NO se ejecuta
- Úsalo para feedback al usuario o flujos alternativos
Acceso a Parámetros
El handler ifRestrictedRunInstead recibe el mismo parámetro que habría sido pasado a la función envuelta. Esto te permite proporcionar feedback consciente del contexto (ej., "Por favor inicia sesión para buscar '{query}'").
Commands NoParam: Para commands NoParam (createAsyncNoParam, createSyncNoParam), el handler ifRestrictedRunInstead no tiene parámetro: void Function() en lugar de RunInsteadHandler<TParam>.
Restricción vs Verificaciones Manuales
❌️ Sin restricciones (verificaciones manuales):
void handleSave() {
if (!isLoggedIn.value) return; // Verificación manual
if (command.isRunning.value) return; // Verificación manual
command.run();
}✅ Con restricciones (automático):
late final command = Command.createAsync<Data, void>(
(data) => api.save(data),
restriction: isLoggedIn.map((logged) => !logged),
);
// UI automáticamente deshabilita cuando está restringido
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('Guardar'),
);
}
}Beneficios:
- UI automáticamente refleja estado
- No se necesitan verificaciones manuales
- Lógica centralizada
- Reactivo a cambios de estado
Errores Comunes
❌️ Invertir la lógica de restricción
// MAL: restriction espera true = deshabilitado
restriction: isLoggedIn, // ¡deshabilitado cuando está logueado (al revés)!// CORRECTO: negar la condición
restriction: isLoggedIn.map((logged) => !logged), // deshabilitado cuando NO está logueado❌️ Usar isRunning para restricciones
// MAL: actualización async puede causar condiciones de carrera
restriction: otherCommand.isRunning,// CORRECTO: usar versión síncrona
restriction: otherCommand.isRunningSync,❌️ Olvidar disponer fuentes de restricción
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(); // ¡No olvides esto!
}
}Ver También
- Fundamentos de Command — Creando y ejecutando commands
- Propiedades del Command — canRun, isRunning, isRunningSync
- Manejo de Errores (Error Handling) — Manejando errores de runtime
- Operadores de listen_it — Operadores de ValueListenable