El autor está disponible para consultoría y trabajo por contratoAgenda una llamada →
Skip to content

Encadenamiento de Commands ​

Conecta commands de forma declarativa usando pipeToCommand. Cuando un ValueListenable fuente cambia, automáticamente dispara el command destino.

¿Por Qué Usar pipeToCommand? ​

En lugar de configurar listeners manualmente:

dart
// ❌ Enfoque manual - más boilerplate
sourceCommand.listen((value, _) {
  if (value.isNotEmpty) {
    targetCommand(value);
  }
});

Usa pipeToCommand para encadenamiento más limpio y declarativo:

dart
// ✅ Enfoque declarativo
sourceCommand
    .where((value) => value.isNotEmpty)
    .pipeToCommand(targetCommand);

Beneficios:

  • Declarativo y legible
  • Se combina con operadores de listen_it (debounce, where, map)
  • Retorna ListenableSubscription para limpieza fácil
  • Funciona con cualquier ValueListenable, no solo commands

Encadenamiento Inline con Cascade ​

El patrón más limpio: usa el operador cascade de Dart .. para encadenar directamente en la definición del command:

dart
class DataManager {
  late final refreshCommand = Command.createAsyncNoParam<List<Data>>(
    () => api.fetchData(),
    initialValue: [],
  );

  // ¡Encadena directamente en la definición - no se necesita constructor!
  late final saveCommand = Command.createAsyncNoResult<Data>(
    (data) => api.save(data),
  )..pipeToCommand(refreshCommand);
}

Esto elimina la necesidad de un constructor solo para configurar pipes. La suscripción se gestiona automáticamente cuando se dispone el command.

Uso Básico ​

pipeToCommand funciona con cualquier ValueListenable:

Desde un Command ​

Cuando un command completa, dispara otro:

dart
class DataManager {
  late final refreshCommand = Command.createAsyncNoParam<List<Data>>(
    () => api.fetchData(),
    initialValue: [],
  );

  // When saveCommand completes, automatically refresh
  late final saveCommand = Command.createAsyncNoResult<Data>(
    (data) => api.save(data),
  )..pipeToCommand(refreshCommand);

  void dispose() {
    saveCommand.dispose();
    refreshCommand.dispose();
  }
}

Desde isRunning ​

Reacciona al estado de ejecución del command:

dart
class SpinnerManager {
  // Command that controls a global spinner
  late final showSpinnerCommand = Command.createSync<bool, bool>(
    (show) => show,
    initialValue: false,
  );

  // When long command starts/stops, update spinner
  late final longRunningCommand = Command.createAsyncNoParam<Data>(
    () async {
      await Future.delayed(Duration(seconds: 5));
      return Data();
    },
    initialValue: Data.empty(),
  )..isRunning.pipeToCommand(showSpinnerCommand);
}

Desde results ​

Pasa el CommandResult completo (incluye estado de éxito/error):

dart
class LoggingManager {
  late final logCommand = Command.createSync<CommandResult<Data, Data>, void>(
    (result) {
      if (result.hasError) {
        debugPrint('Save failed: ${result.error}');
      } else if (result.hasData) {
        debugPrint('Save succeeded: ${result.data}');
      }
    },
    initialValue: null,
  );

  // Pipe all results (success/error) to logging
  late final saveCommand = Command.createAsync<Data, Data>(
    (data) async {
      await api.save(data);
      return data;
    },
    initialValue: Data.empty(),
  )..results.pipeToCommand(logCommand);
}

Desde ValueNotifier ​

También funciona con ValueNotifier simple:

dart
class FormManager {
  late final loadUserCommand = Command.createAsync<String, User>(
    (userId) => api.login(userId, ''),
    initialValue: User.empty(),
  );

  // When user ID changes, load user details
  late final selectedUserId = ValueNotifier<String>('')
    ..pipeToCommand(loadUserCommand);

  void dispose() {
    selectedUserId.dispose();
    loadUserCommand.dispose();
  }
}

Función Transform ​

Cuando los tipos de fuente y destino no coinciden, usa el parámetro transform:

Transform Básico ​

dart
class UserManager {
  // Command expects String, but we have int
  late final fetchUserCommand = Command.createAsync<String, User>(
    (userId) => api.login(userId, ''),
    initialValue: User.empty(),
  );

  // Transform int to String
  late final selectedUserId = ValueNotifier<int>(0)
    ..pipeToCommand(fetchUserCommand, transform: (id) => id.toString());
}

Transform Complejo ​

Crea objetos de parámetros complejos:

dart
class FetchParams {
  final String userId;
  final bool includeDetails;

  FetchParams(this.userId, {this.includeDetails = true});
}

class DetailedUserManager {
  late final fetchUserCommand = Command.createAsync<FetchParams, User>(
    (params) => api.login(params.userId, ''),
    initialValue: User.empty(),
  );

  // Transform simple ID to complex params object
  late final selectedUserId = ValueNotifier<String>('')
    ..pipeToCommand(
      fetchUserCommand,
      transform: (id) => FetchParams(id, includeDetails: true),
    );
}

Transform de Resultados ​

Transforma resultados del command antes de pasar:

dart
class ResultTransformManager {
  // Notification command only needs a message
  late final notifyCommand = Command.createSync<String, void>(
    (message) => debugPrint('Notification: $message'),
    initialValue: null,
  );

  // Transform Data result to notification message
  late final saveCommand = Command.createAsync<Data, Data>(
    (data) async {
      await api.save(data);
      return data;
    },
    initialValue: Data.empty(),
  )..pipeToCommand(notifyCommand,
      transform: (data) => 'Saved item: ${data.id}');
}

Manejo de Tipos ​

pipeToCommand maneja tipos automáticamente:

  1. Transform proporcionado → Usa la función transform
  2. Tipos coinciden → Pasa el valor directamente a target.run(value)
  3. Tipos no coinciden → Llama target.run() sin parámetros

Esto significa que puedes pasar a commands sin parámetros sin un transform:

dart
// saveCommand retorna Data, refreshCommand no toma params
saveCommand.pipeToCommand(refreshCommand);  // ¡Funciona! Llama refreshCommand.run()

Combinando con Operadores de listen_it ​

El verdadero poder viene de combinar pipeToCommand con operadores de listen_it como debounce, where, y map:

Búsqueda con Debounce ​

dart
class SearchManager {
  late final textChangedCommand = Command.createSync<String, String>(
    (s) => s,
    initialValue: '',
  );

  late final searchCommand = Command.createAsync<String, List<Result>>(
    (query) => api.search(query),
    initialValue: [],
  );

  late final ListenableSubscription _subscription;

  SearchManager() {
    // Debounce + filter + pipe to search command
    _subscription = textChangedCommand
        .debounce(Duration(milliseconds: 500))
        .where((text) => text.length >= 3)
        .pipeToCommand(searchCommand);
  }

  void dispose() {
    _subscription.cancel();
    textChangedCommand.dispose();
    searchCommand.dispose();
  }
}

Filtrar Antes de Pasar ​

dart
class FilteredPipeManager {
  late final inputCommand = Command.createSync<int, int>(
    (n) => n,
    initialValue: 0,
  );

  late final processCommand = Command.createAsync<int, String>(
    (n) async => 'Processed: $n',
    initialValue: '',
  );

  late final ListenableSubscription _subscription;

  FilteredPipeManager() {
    // Only pipe positive numbers, debounced
    _subscription = inputCommand
        .where((n) => n > 0)
        .debounce(Duration(milliseconds: 200))
        .pipeToCommand(processCommand);
  }

  void dispose() {
    _subscription.cancel();
    inputCommand.dispose();
    processCommand.dispose();
  }
}

Gestión de Suscripciones ​

pipeToCommand retorna un ListenableSubscription. Sin embargo, usualmente no necesitas gestionarlo manualmente.

Limpieza Automática

Cuando los commands y pipes están en el mismo objeto (clase manager/servicio), la suscripción es automáticamente recolectada por el garbage collector cuando el objeto contenedor se vuelve inalcanzable. ¡No se necesita limpieza manual!

Ver Gestión de Memoria de listen_it para detalles.

Para casos donde necesitas control manual (ej., pipes creados dinámicamente con operadores):

dart
class CleanupManager {
  late final sourceCommand = Command.createSync<String, String>(
    (s) => s,
    initialValue: '',
  );

  late final targetCommand = Command.createAsyncNoResult<String>(
    (s) async => api.saveContent(s),
  );

  // Store subscription for cleanup
  late final ListenableSubscription _subscription;

  CleanupManager() {
    _subscription = sourceCommand.pipeToCommand(targetCommand);
  }

  void dispose() {
    // Cancel subscription first
    _subscription.cancel();
    // Then dispose commands
    sourceCommand.dispose();
    targetCommand.dispose();
  }
}

Advertencia: Pipes Circulares ​

Evita Pipes Circulares

Nunca crees cadenas de pipes circulares - causan loops infinitos:

dart
// ❌ PELIGRO: ¡Loop infinito!
commandA.pipeToCommand(commandB);
commandB.pipeToCommand(commandA);  // A dispara B dispara A dispara B...

Referencia de API ​

dart
extension ValueListenablePipe<T> on ValueListenable<T> {
  ListenableSubscription pipeToCommand<TTargetParam, TTargetResult>(
    Command<TTargetParam, TTargetResult> target, {
    TTargetParam Function(T value)? transform,
  })
}

Parámetros:

  • target — El command a disparar cuando la fuente cambia
  • transform — Función opcional para convertir el valor fuente al tipo de parámetro destino

Retorna: ListenableSubscription — Cancela esto para detener el pipe

Ver También ​

Publicado bajo la Licencia MIT. Creado por Thomas Burkhart.