Skip to content

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 - canRun refleja restricciones instantáneamente
  • Lógica centralizada - Sin verificaciones if dispersas 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

dart
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:

dart
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:

  1. Crea un ValueNotifier<bool> para trackear estado (isLoggedIn)
  2. Mapéalo a lógica de restricción: !logged significa "restringir cuando NO está logueado"
  3. El command automáticamente actualiza la propiedad canRun
  4. Usa watchValue() para observar canRun en tu widget
  5. El botón automáticamente se deshabilita cuando canRun es 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:

dart
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:

  1. saveCommand usa loadCommand.isRunningSync como restricción
  2. Mientras carga, saveCommand no puede ejecutarse
  3. updateCommand usa combineLatest para combinar ambos estados de ejecución
  4. Update está deshabilitado si CUALQUIERA de load O save está ejecutándose
  5. Demuestra combinar múltiples restricciones con operadores de listen_it

¿Por qué isRunningSync?

  • isRunning se actualiza asíncronamente para evitar condiciones de carrera en rebuilding de UI
  • isRunningSync se actualiza inmediatamente
  • Previene condiciones de carrera en restricciones
  • Usa isRunning para UI, isRunningSync para restricciones

Propiedad canRun

canRun automáticamente combina estado de ejecución y restricciones:

dart
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

dart
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

dart
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:

dart
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:

dart
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
dart
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:

  1. El handler recibe el parámetro que fue pasado al command
  2. Se llama solo cuando restriction es true (command está deshabilitado)
  3. La función envuelta original NO se ejecuta
  4. Ú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):

dart
void handleSave() {
  if (!isLoggedIn.value) return; // Verificación manual
  if (command.isRunning.value) return; // Verificación manual
  command.run();
}

✅ Con restricciones (automático):

dart
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

dart
// MAL: restriction espera true = deshabilitado
restriction: isLoggedIn, // ¡deshabilitado cuando está logueado (al revés)!
dart
// CORRECTO: negar la condición
restriction: isLoggedIn.map((logged) => !logged), // deshabilitado cuando NO está logueado

❌️ Usar isRunning para restricciones

dart
// MAL: actualización async puede causar condiciones de carrera
restriction: otherCommand.isRunning,
dart
// CORRECTO: usar versión síncrona
restriction: otherCommand.isRunningSync,

❌️ Olvidar disponer fuentes de restricción

dart
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

Publicado bajo la Licencia MIT.