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:
// ❌ Enfoque manual - más boilerplate
sourceCommand.listen((value, _) {
if (value.isNotEmpty) {
targetCommand(value);
}
});Usa pipeToCommand para encadenamiento más limpio y declarativo:
// ✅ Enfoque declarativo
sourceCommand
.where((value) => value.isNotEmpty)
.pipeToCommand(targetCommand);Beneficios:
- Declarativo y legible
- Se combina con operadores de
listen_it(debounce, where, map) - Retorna
ListenableSubscriptionpara 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:
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:
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:
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):
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:
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
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:
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:
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:
- Transform proporcionado → Usa la función transform
- Tipos coinciden → Pasa el valor directamente a
target.run(value) - Tipos no coinciden → Llama
target.run()sin parámetros
Esto significa que puedes pasar a commands sin parámetros sin un transform:
// 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
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
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):
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:
// ❌ PELIGRO: ¡Loop infinito!
commandA.pipeToCommand(commandB);
commandB.pipeToCommand(commandA); // A dispara B dispara A dispara B...Referencia de API
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 cambiatransform— 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
- Restricciones — Deshabilitar commands basado en estado
- Propiedades del Command — Propiedades observables como
isRunning - Operadores de listen_it — Operadores como
debounce,where,map