Primeros Pasos
command_it es una forma de gestionar tu estado basada en ValueListenable y el patrón de diseño Command. Un Command es un objeto que envuelve una función, haciéndola invocable mientras proporciona actualizaciones de estado reactivas—perfecto para conectar tu UI con la lógica de negocio.
Instalación
Añade a tu pubspec.yaml:
dependencies:
command_it: ^2.0.0Para la configuración recomendada con watch_it y get_it, simplemente importa flutter_it:
dependencies:
flutter_it: ^1.0.0¿Por Qué Commands?
Cuando empecé con Flutter, el enfoque más recomendado era BLoC. Pero enviar objetos a un StreamController para disparar procesos nunca me pareció correcto—debería sentirse como llamar a una función. Viniendo del mundo .NET, estaba acostumbrado a Commands: objetos invocables que automáticamente deshabilitan su botón disparador mientras se ejecutan y emiten resultados de forma reactiva.
Porté este concepto a Dart con rx_command, pero los Streams se sentían pesados. Después de que Remi Rousselet me convenciera de lo más simples que son los ValueNotifiers, creé command_it: toda la potencia del patrón Command, cero Streams, 100% ValueListenable.
Concepto Central
Un Command es:
- Un envoltorio de función - Encapsula funciones sync/async como objetos invocables
- Un ValueListenable - Publica resultados de forma reactiva para que tu UI pueda observar cambios
- Tipado seguro -
Command<TParam, TResult>dondeTParames el tipo de entrada yTResultes el tipo de salida
El Patrón Command
La filosofía central: Inicia commands con run() (dispara y olvida), luego tu app/UI observa y reacciona a sus cambios de estado. Este patrón reactivo mantiene tu UI responsiva sin bloqueos—disparas la acción y dejas que tu UI responda automáticamente a estados de carga, resultados y errores.
Aquí está el ejemplo más simple posible usando watch_it (el enfoque recomendado):
class CounterService {
int _count = 0;
// Command wraps a function and acts as a ValueListenable
late final incrementCommand = Command.createSyncNoParam<String>(
() {
_count++;
return _count.toString();
},
initialValue: '0',
);
}
// Register with get_it (call this in main())
void setup() {
GetIt.instance.registerSingleton(CounterService());
}
// Use watch_it to observe the command
class CounterWidget extends WatchingWidget {
const CounterWidget({super.key});
@override
Widget build(BuildContext context) {
// Watch the command value - rebuilds when it changes
final count = watchValue((CounterService s) => s.incrementCommand);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You have pushed the button this many times:'),
Text(
count,
style: Theme.of(context).textTheme.headlineMedium,
),
SizedBox(height: 16),
// No parameters - use .run as tearoff
ElevatedButton(
onPressed: GetIt.instance<CounterService>().incrementCommand.run,
child: Text('Increment'),
),
],
);
}
}Puntos clave:
- Crea con
Command.createSyncNoParam<TResult>()(ver Tipos de Command para diferentes firmas) - Command tiene un método
.run- úsalo como tearoff paraonPressed - Usa
watchValuepara observar el command - se reconstruye automáticamente cuando el valor cambia - Registra tu servicio con
get_it(llama a setup enmain()), extiendeWatchingWidgetpara la funcionalidad dewatch_it - El valor inicial es requerido para que la UI tenga algo que mostrar inmediatamente
Usando Commands sin watch_it
Los Commands también funcionan con ValueListenableBuilder simple si prefieres no usar watch_it. Ver Sin watch_it para ejemplos. Para más información sobre watch_it, consulta la documentación de watch_it.
Ejemplo Real: Commands Async con Estados de Carga
La mayoría de apps reales necesitan operaciones async (llamadas HTTP, consultas a base de datos, etc.). Los Commands hacen esto trivial al rastrear el estado de ejecución automáticamente. Aquí hay un ejemplo con watch_it:
class WeatherService {
late final loadWeatherCommand = Command.createAsync<String, String>(
(city) async {
await simulateDelay(1000);
return 'Weather in $city: Sunny, 72°F';
},
initialValue: 'No data loaded',
);
}
// Register service with get_it (call this in main())
void setup() {
GetIt.instance.registerSingleton(WeatherService());
}
// Use watch_it to observe commands without ValueListenableBuilder
class WeatherWidget extends WatchingWidget {
const WeatherWidget({super.key});
@override
Widget build(BuildContext context) {
// Watch the command value directly
final weather = watchValue((WeatherService s) => s.loadWeatherCommand);
// Watch the loading state
final isLoading =
watchValue((WeatherService s) => s.loadWeatherCommand.isRunning);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (isLoading)
CircularProgressIndicator()
else
Text(weather, style: TextStyle(fontSize: 18)),
SizedBox(height: 16),
// With parameters - call command directly (it's callable)
ElevatedButton(
onPressed: () =>
GetIt.instance<WeatherService>().loadWeatherCommand('London'),
child: Text('Load Weather'),
),
],
);
}
}Qué está pasando:
Command.createAsync<TParam, TResult>()envuelve una función asyncwatchValueobserva tanto el resultado del command COMO su propiedadisRunning- La UI automáticamente muestra un indicador de carga mientras el command se ejecuta
- Sin widgets
ValueListenableBuilderanidados -watch_itmantiene el código limpio - El parámetro del command (
'London') se pasa a la función envuelta
Este patrón elimina el boilerplate de rastrear manualmente estados de carga y builders anidados → commands + watch_it manejan todo por ti.
Los Commands Siempre Notifican (Por Defecto)
Los Commands notifican a los listeners en cada ejecución, incluso si el valor del resultado es idéntico. Esto es intencional porque:
- Las acciones del usuario necesitan feedback - Al hacer clic en "Actualizar", los usuarios esperan indicadores de carga incluso si los datos no han cambiado
- El estado cambia durante la ejecución -
isRunning,CommandResulty los estados de error se actualizan durante la operación async - La acción importa, no solo el resultado - El command se ejecutó (API llamada, archivo guardado), lo cual es importante independientemente del valor de retorno
Cuándo usar notifyOnlyWhenValueChanges: true:
- Commands de cómputo puro donde solo importa el resultado
- Actualizaciones de alta frecuencia donde resultados idénticos deberían ignorarse
- Optimización de rendimiento cuando los listeners son costosos
Para la mayoría de escenarios reales con acciones de usuario y operaciones async, el comportamiento por defecto es lo que quieres.
Conceptos Clave de un Vistazo
command_it ofrece características potentes para apps en producción:
Propiedades del Command
El command mismo es un ValueListenable<TResult> que publica el resultado. Los Commands también exponen propiedades observables adicionales:
value- Getter de propiedad para el resultado actual (no es un ValueListenable, solo el valor)isRunning-ValueListenable<bool>que indica si el command se está ejecutando actualmente (solo commands async)canRun-ValueListenable<bool>combinando!isRunning && !restriction(ver restricciones abajo)errors-ValueListenable<CommandError?>de errores de ejecución
Ver Propiedades del Command para detalles.
CommandResult
En lugar de observar múltiples propiedades por separado, usa results para obtener estado comprehensivo:
command.results // ValueListenable<CommandResult<TParam, TResult>>CommandResult combina data, error, isRunning y paramData en un objeto. Perfecto para estados de UI comprehensivos de error/carga/éxito.
Ver Command Results para detalles.
Control de Progreso
Rastrea el progreso de operaciones, muestra mensajes de estado y permite cancelación con la característica integrada de Control de Progreso:
final uploadCommand = Command.createAsyncWithProgress<File, String>(
(file, handle) async {
for (int i = 0; i <= 100; i += 10) {
if (handle.isCanceled.value) return 'Canceled';
await uploadChunk(file, i);
handle.updateProgress(i / 100.0);
handle.updateStatusMessage('Uploading: $i%');
}
return 'Complete';
},
initialValue: '',
);// En UI:
watchValue((MyService s) => s.uploadCommand.progress) // 0.0 a 1.0
watchValue((MyService s) => s.uploadCommand.statusMessage) // Texto de estado
uploadCommand.cancel() // Solicitar cancelaciónTodos los commands exponen propiedades de progreso (incluso sin factory WithProgress) - los commands sin progreso simplemente devuelven valores por defecto con cero overhead.
Ver Control de Progreso para detalles.
Manejo de Errores (Error Handling)
Los Commands capturan excepciones automáticamente y las publican via la propiedad errors. Puedes usar operadores de listen_it para filtrar y manejar tipos de error específicos:
command.errors.where((error) => error?.error is NetworkError).listen((error, _) {
showSnackbar('Error de red: ${error!.error.message}');
});Para escenarios avanzados, usa filtros de error para enrutar diferentes tipos de error a nivel de command. Ver Manejo de Errores para detalles.
Restricciones
Controla cuándo un command puede ejecutarse pasando un ValueListenable<bool> como restricción:
final isOnline = ValueNotifier(true);
final command = Command.createAsync(
fetchData,
initialValue: [],
restriction: isOnline, // El command solo se ejecuta cuando isOnline.value == true
);Debido a que es un ValueNotifier pasado al constructor, un command puede habilitarse y deshabilitarse en cualquier momento cambiando el valor del notifier.
Ver Restricciones para detalles.
Siguientes Pasos
Elige tu ruta de aprendizaje basándote en tu objetivo:
📚 Quiero aprender los fundamentos
Empieza con Fundamentos de Command para entender:
- Todos los métodos factory de command (sync/async, con/sin parámetros)
- Cómo ejecutar commands programáticamente vs. con triggers de UI
- Valores de retorno y valores iniciales
⚡ Quiero construir una característica real
Sigue el Tutorial de App del Clima para construir una característica completa:
- Commands async con llamadas API reales
- Debouncing de entrada de usuario
- Estados de carga y manejo de errores
- Restricciones de command
- Múltiples commands trabajando juntos
🛡️ Necesito manejo de errores robusto
Revisa Manejo de Errores:
- Capturar y mostrar errores
- Enrutar diferentes tipos de error a diferentes handlers
- Lógica de reintento y estrategias de fallback
🎯 Quiero patrones listos para producción
Ver Mejores Prácticas para:
- Cuándo usar commands vs. otros patrones
- Evitar errores comunes
- Optimización de rendimiento
- Recomendaciones de arquitectura
🧪 Necesito escribir tests
Ve a Testing para:
- Testing unitario de commands en aislamiento
- Widget testing con commands
- Mocking de respuestas de command
- Testing de escenarios de error
Referencia Rápida
| Tema | Enlace |
|---|---|
| Crear commands (todos los métodos factory) | Fundamentos de Command |
| Tipos de command (firmas) | Tipos de Command |
| Propiedades observables (value, isRunning, etc.) | Propiedades del Command |
| CommandResult (estado comprehensivo) | Command Results |
| Widget CommandBuilder | Command Builders |
| Manejo de errores y enrutamiento | Manejo de Errores |
| Ejecución condicional | Restricciones |
| Patrones de testing | Testing |
Integración con watch_it | Observando Commands con watch_it |
| Patrones de producción | Mejores Prácticas |
¡Listo para profundizar? ¡Elige un tema de la Referencia Rápida de arriba o sigue una de las rutas de aprendizaje en Siguientes Pasos!