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.