Fundamentos de Command
Aprende cómo crear y ejecutar commands, la base de command_it.
Los Ejemplos Usan watch_it
Todos los ejemplos usan watch_it para observar commands. Ver Sin watch_it si prefieres ValueListenableBuilder.
¿Qué es un Command?
Un Command envuelve una función (sync o async) y la hace observable. En lugar de llamar una función directamente y rastrear manualmente su estado, creas un command que:
- Ejecuta tu función cuando es llamado
- Rastrea automáticamente el estado de ejecución (
isRunning) - Publica resultados via
ValueListenable - Maneja errores de forma elegante
- Previene ejecución paralela
Piénsalo como: Una función + gestión de estado automática + notificaciones reactivas.
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.
Creando Tu Primer Command
Los Commands se crean usando funciones factory estáticas, no constructores. El tipo más común es createAsyncNoParam para funciones async sin parámetros:
// 1. Create a service with a command
class CounterService {
int _counter = 0;
late final incrementCommand = Command.createAsyncNoParam<String>(
() async {
await Future.delayed(Duration(milliseconds: 500));
_counter++;
return _counter.toString();
},
initialValue: '0',
);
}
// Register with get_it (call this in main())
void setup() {
GetIt.instance.registerSingleton(CounterService());
}
// 2. Use watch_it to observe the command
class CounterWidget extends WatchingWidget {
const CounterWidget({super.key});
@override
Widget build(BuildContext context) {
// Watch the command value
final count = watchValue((CounterService s) => s.incrementCommand);
// Watch the loading state
final isRunning =
watchValue((CounterService s) => s.incrementCommand.isRunning);
return Column(
children: [
// Shows loading indicator automatically while command runs
if (isRunning) CircularProgressIndicator() else Text('Count: $count'),
SizedBox(height: 16),
ElevatedButton(
onPressed: GetIt.instance<CounterService>().incrementCommand.run,
child: Text('Increment'),
),
],
);
}
}Qué sucede:
- El Command envuelve tu función async
- Cuando se llama
run(), la función se ejecuta - Mientras se ejecuta,
isRunningestrue - El resultado se publica en la propiedad
value - La UI se reconstruye automáticamente via
watchValue
Ejecutando Commands
Hay dos formas de ejecutar un command:
1. Usando run() (Dispara y Olvida)
// Llama al método run del command
loadDataCommand.run();
// O con un parámetro
searchCommand.run('flutter');Usa run() cuando quieras disparar la ejecución sin esperar el resultado. Perfecto para handlers de botones.
2. Llamando como Clase Callable
Los Commands son clases callable, así que puedes invocarlos directamente:
// Callable - igual que run()
loadDataCommand();
// Con parámetro
searchCommand('flutter');Esto es solo una abreviatura para run() - no devuelve un valor.
¿Por Qué Usar .run para Tearoffs?
En el pasado, era posible pasar clases callable directamente como tearoffs. Sin embargo, debido a cambios en Dart, esto ya no es posible. Para VoidCallbacks opcionales (como onPressed), pasar una clase callable directamente es ahora un error de compilación. Incluso cuando compila, dispara la advertencia del linter implicit_call_tearoffs porque Dart implícitamente hace tearoff del método .call(), lo cual se considera poco claro.
Siempre usa .run para tearoffs:
// ✅ Bien - tearoff explícito
ElevatedButton(onPressed: command.run, ...)
// ❌ Evitar - implicit call tearoff (error de compilación para VoidCallback opcional)
ElevatedButton(onPressed: command, ...)Por esto command_it renombró de execute() a run() en v9.0.0 - haciendo del método explícito la API principal.
3. Usando runAsync() (Await del Resultado)
Usa runAsync() cuando necesites hacer await del resultado:
final result = await loadDataCommand.runAsync();Usar con Moderación
runAsync() rompe el patrón de dispara-y-olvida descrito arriba. Solo úsalo cuando una API requiere que se devuelva un Future (como RefreshIndicator.onRefresh). Para código de aplicación normal, siempre usa run() y observa cambios de estado de forma reactiva.
Perfecto para RefreshIndicator:
RefreshIndicator(
onRefresh: () => updateCommand.runAsync(),
child: ListView(...),
)Commands con Parámetro y Tipo de Retorno
La mayoría de commands necesitan tanto parámetros como valores de retorno. Usa createAsync<TParam, TResult> para funciones async con parámetro y resultado:
late final searchCommand = Command.createAsync<String, List<Todo>>(
(query) async {
await Future.delayed(Duration(milliseconds: 500));
return fakeTodos.where((t) => t.title.contains(query)).toList();
},
initialValue: [],
);
// Llamar con parámetro
searchCommand.run('flutter');Parámetros de tipo:
- Primer tipo (
String) = tipo del parámetro - Segundo tipo (
List<Todo>) = tipo del resultado
Commands Síncronos
Para funciones síncronas, usa createSync:
late final formatCommand = Command.createSync<String, String>(
(text) => text.toUpperCase(),
initialValue: '',
);
// Usar exactamente como commands async
formatCommand.run('hello');Importante: Los commands sync no soportan isRunning - accederlo lanzará una excepción porque la UI no puede actualizarse mientras las funciones síncronas se ejecutan.
Valores Iniciales
Los Commands que devuelven un valor requieren un initialValue:
Command.createAsyncNoParam<List<Todo>>(
() => api.fetchTodos(),
initialValue: [], // Requerido: ¿qué valor antes de la primera ejecución?
);¿Por qué? Los Commands son ValueListenable<TResult>. Necesitan un valor desde el inicio, antes de que la primera ejecución complete. Esto es especialmente importante si el valor del command debe mostrarse en un widget—los widgets necesitan un valor en el primer build incluso si el command no ha sido ejecutado aún.
Los Commands que devuelven void no necesitan valores iniciales:
Command.createAsyncNoResult<String>(
(message) => api.sendMessage(message),
// No se necesita initialValue
);Prevención Automática de Ejecución Paralela
Los Commands automáticamente previenen la ejecución paralela:
final saveCommand = Command.createAsyncNoParam<void>(
() async {
await Future.delayed(Duration(seconds: 2));
await api.save();
},
);
// Clic rápido en botón
saveCommand.run(); // Inicia ejecución
saveCommand.run(); // Ignorado - ya ejecutándose
saveCommand.run(); // Ignorado - ya ejecutándose
// ... pasan 2 segundos ...
saveCommand.run(); // Ahora este se ejecutaEsto previene:
- Doble envío
- Condiciones de carrera
- Llamadas API desperdiciadas
Usando Commands en Managers
Mejor práctica: Crea commands en clases manager/controller, no en widgets:
class TodoManager {
final api = ApiClient();
late final loadTodosCommand = Command.createAsyncNoParam<List<Todo>>(
() => api.fetchTodos(),
initialValue: [],
);
late final saveTodoCommand = Command.createAsyncNoResult<Todo>(
(todo) => api.saveTodo(todo),
);
}
// En widget
class TodoListWidget extends StatelessWidget {
final manager = TodoManager();
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<List<Todo>>(
valueListenable: manager.loadTodosCommand,
builder: (context, todos, _) => ListView(...),
);
}
}¿Por qué?
- Separa lógica de negocio de UI
- Más fácil de testear
- Reutilizable entre widgets
- Límites de responsabilidad claros
Disposing Commands
Los Commands deben ser disposed para prevenir memory leaks:
class TodoManager {
late final loadCommand = Command.createAsyncNoParam<List<Todo>>(...);
void dispose() {
loadCommand.dispose();
}
}Al usar StatefulWidget:
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final TodoManager manager;
@override
void initState() {
super.initState();
manager = TodoManager();
}
@override
void dispose() {
manager.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => ...;
}Con get_it: Registra como singleton y haz dispose al cerrar la app o usa scopes para limpieza automática. Con watch_it: Usa createOnce() para gestión automática del ciclo de vida.
Ver También
- Propiedades del Command — value, isRunning, canRun, errors, results
- Tipos de Command — Todas las funciones factory
- Manejo de Errores — Manejando errores elegantemente
- Mejores Prácticas — Patrones de producción