Command Builders
Simplifica la integración de UI con commands usando CommandBuilder - un widget que maneja todos los estados del command (carga, datos, error) con mínimo boilerplate.
¿Por Qué Usar CommandBuilder?
En lugar de construir manualmente widgets ValueListenableBuilder para command.results, usa CommandBuilder para manejar declarativamente todos los estados del command:
// En lugar de esto:
ValueListenableBuilder<CommandResult<void, String>>(
valueListenable: command.results,
builder: (context, result, _) {
if (result.isRunning) return CircularProgressIndicator();
if (result.hasError) return Text('Error: ${result.error}');
return Text('Contador: ${result.data}');
},
)
// Usa esto:
CommandBuilder(
command: command,
whileRunning: (context, _, __) => CircularProgressIndicator(),
onError: (context, error, _, __) => Text('Error: $error'),
onData: (context, value, _) => Text('Contador: $value'),
)Beneficios:
- Código más limpio y declarativo
- Builders separados para cada estado
- Menos anidación que ValueListenableBuilder
- Acceso a parámetros con type-safety
Ejemplo Básico
class CounterWidgetWithBuilder extends StatelessWidget {
const CounterWidgetWithBuilder({
super.key,
required this.manager,
});
final CounterManager manager;
@override
Widget build(BuildContext context) {
return Column(
children: [
CommandBuilder(
command: manager.incrementCommand,
whileRunning: (context, _, __) => CircularProgressIndicator(),
onData: (context, value, _) => Text('Count: $value'),
onError: (context, error, _, __) => Text('Error: $error'),
),
ElevatedButton(
onPressed: manager.incrementCommand.run,
child: Text('Increment'),
),
],
);
}
}Parámetros
Todos los parámetros son opcionales excepto command:
Tipos Genéricos:
TParam- El parámetro que se pasó cuando se llamó al command (ej., la consulta de búsqueda)TResult- El valor de retorno de la ejecución del command
| Parámetro | Tipo | Descripción |
|---|---|---|
| command | Command<TParam, TResult> | Requerido. El command a observar |
| onData | Widget Function(BuildContext, TResult, TParam?) | Builder para ejecución exitosa con valor de retorno |
| onSuccess | Widget Function(BuildContext, TParam?) | Builder para ejecución exitosa (ignora valor de retorno) |
| onNullData | Widget Function(BuildContext, TParam?) | Builder cuando el command retorna null |
| whileRunning | Widget Function(BuildContext, TResult?, TParam?) | Builder mientras el command se ejecuta |
| onError | Widget Function(BuildContext, Object, TResult?, TParam?) | Builder cuando el command lanza error |
| runCommandOnFirstBuild | bool | Si es true, ejecuta el command en initState (por defecto: false) |
| initialParam | TParam? | Parámetro a pasar cuando runCommandOnFirstBuild es true |
Cuándo Usar Cada Builder
onData - Commands con valores de retorno:
CommandBuilder(
command: searchCommand,
onData: (context, items, query) => ItemList(items), // ✅ Usa items
)onSuccess - Commands void o cuando no necesitas el resultado:
CommandBuilder(
command: deleteCommand,
onSuccess: (context, deletedItem) => Text('Eliminado: ${deletedItem?.name}'),
)onNullData - Manejar resultados null explícitamente:
CommandBuilder(
command: fetchCommand,
onData: (context, data, _) => DataWidget(data),
onNullData: (context, _) => Text('No hay datos disponibles'),
)whileRunning - Mostrar estado de carga:
whileRunning: (context, lastValue, param) => Column(
children: [
CircularProgressIndicator(),
if (lastValue != null) Text('Anterior: $lastValue'), // Mostrar datos obsoletos
if (param != null) Text('Cargando: $param'),
],
)onError - Manejar errores:
onError: (context, error, lastValue, param) => ErrorWidget(
error: error,
onRetry: () => command(param), // Reintentar con mismo parámetro
)TIP
El parámetro lastValue en whileRunning y onError solo contendrá datos si el command fue creado con includeLastResultInCommandResults: true. De lo contrario, siempre será null. Ver includeLastResultInCommandResults.
Mostrando Parámetro en UI
Accede al parámetro del command en cualquier builder:
CommandBuilder(
command: searchCommand,
whileRunning: (context, _, query) => Text('Buscando: $query'),
onData: (context, items, query) => Column(
children: [
Text('Resultados para: $query'),
ItemList(items),
],
),
onError: (context, error, _, query) => Text('Búsqueda "$query" falló: $error'),
)Ejecutando Commands Automáticamente al Montar
CommandBuilder puede ejecutar automáticamente un command cuando el widget se construye por primera vez usando el parámetro runCommandOnFirstBuild. Esto es particularmente útil cuando no usas watch_it (que proporciona callOnce para este propósito).
Uso Básico (Sin Parámetro)
CommandBuilder(
command: loadTodosCommand,
runCommandOnFirstBuild: true, // Ejecuta command en initState
whileRunning: (context, _, __) => CircularProgressIndicator(),
onData: (context, todos, _) => TodoList(todos),
onError: (context, error, _, __) => ErrorWidget(error),
)Qué sucede:
- El widget se construye
- El command se ejecuta automáticamente en
initState - La UI muestra estado de carga → estado de datos/error
- El command solo se ejecuta una vez - no en rebuilds
Con Parámetros
Usa initialParam para pasar un parámetro al command:
CommandBuilder(
command: searchCommand,
runCommandOnFirstBuild: true,
initialParam: 'flutter', // Parámetro a pasar
whileRunning: (context, _, query) => Text('Buscando: $query'),
onData: (context, items, query) => ItemList(items),
onError: (context, error, _, query) => Text('Búsqueda falló: $error'),
)Cuándo Usar
✅ Usa runCommandOnFirstBuild cuando:
- ✅ No usas
watch_it(sin acceso acallOnce) - ✅ El widget debe cargar sus propios datos al montar
- ✅ Quieres widgets de carga de datos autocontenidos
- ✅ Escenarios simples de fetch de datos
❌️ No uses cuando:
- ❌️ Usas
watch_it- prefierecallOnceen su lugar (separación más clara) - ❌️ El
Commandya se está ejecutando en otro lugar - ❌️ Necesitas lógica condicional antes de ejecutar
Comparación con callOnce de watch_it
Con watch_it (recomendado si usas watch_it):
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
callOnce((Manager m) => m.loadTodos()); // Trigger explícito
return CommandBuilder(
command: getIt<Manager>().loadTodos,
onData: (context, todos, _) => TodoList(todos),
);
}
}Sin watch_it (usa runCommandOnFirstBuild):
class TodoWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CommandBuilder(
command: getIt<Manager>().loadTodos,
runCommandOnFirstBuild: true, // Trigger incorporado
onData: (context, todos, _) => TodoList(todos),
);
}
}Reglas de Precedencia de Builders
Tanto CommandBuilder como CommandResult.toWidget() usan las mismas reglas de precedencia para determinar qué builder llamar:
Orden de precedencia completo:
if (error != null)→ llamaonErrorif (isRunning)→ llamawhileRunningif (onSuccess != null)→ llamaonSuccess⚠️ ¡Tiene prioridad sobre onData!if (data != null)→ llamaonDataelse→ llamaonNullData
onData vs onSuccess
Cuando el command completa exitosamente:
- Si
onSuccessestá proporcionado → llámalo (no verifica si data es null) - Si no, si data != null → llama
onData - Si no → llama
onNullData
Elige onSuccess cuando:
- El command retorna void (ej.,
Command.createAsyncNoResult) - Solo necesitas mostrar mensaje de confirmación/éxito
- Los datos del resultado son irrelevantes para la UI
Elige onData cuando:
- El command retorna datos que necesitas mostrar/usar
- Quieres manejar datos no-null diferente de datos null
Método de Extensión toWidget()
El método de extensión .toWidget() en CommandResult proporciona el mismo patrón de builder declarativo que CommandBuilder, pero para usar cuando ya tienes acceso a un CommandResult (ej., via watch_it, provider, o flutter_hooks).
class WeatherToWidgetExample extends WatchingWidget {
@override
Widget build(BuildContext context) {
final results = watchValue(
(WeatherManager m) => m.loadWeatherCommand.results,
);
return results.toWidget(
onData: (weather, param) {
return ListView.builder(
itemCount: weather.length,
itemBuilder: (context, index) {
final entry = weather[index];
return ListTile(
title: Text(entry.city),
subtitle: Text(entry.condition),
trailing: Text('${entry.temperature}°F'),
);
},
);
},
whileRunning: (lastWeather, param) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading weather for ${param ?? ""}...'),
],
),
);
},
onError: (error, lastWeather, param) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, color: Colors.red, size: 48),
SizedBox(height: 16),
Text('Error: $error'),
if (param != null) Text('For city: $param'),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => weatherManager.loadWeatherCommand('London'),
child: Text('Retry'),
),
],
),
);
},
);
}
}Parámetros:
Debes proporcionar al menos uno de estos dos:
onData-Widget Function(TResult result, TParam? param)?- Se llama cuando el command tiene datos no-null (solo si
onSuccessno está proporcionado) - Recibe tanto los datos del resultado como el parámetro
- Usa para commands que retornan datos que necesitas mostrar
- Se llama cuando el command tiene datos no-null (solo si
onSuccess-Widget Function(TParam? param)?- Se llama en completación exitosa (sin error, no ejecutándose)
- NO recibe datos del resultado, solo el parámetro
- Tiene prioridad sobre
onDatasi ambos están proporcionados - Usa para commands que retornan void o cuando no necesitas el valor del resultado
Builders opcionales:
whileRunning-Widget Function(TResult? lastResult, TParam? param)?- Se llama mientras el command se ejecuta
- Recibe último resultado (si
includeLastResultInCommandResults: true) y parámetro
onError-Widget Function(Object error, TResult? lastResult, TParam? param)?- Se llama cuando ocurre un error
- Recibe error, último resultado y parámetro
onNullData-Widget Function(TParam? param)?- Se llama cuando data es null (solo si ni
onSuccessnionDatalo manejan) - Recibe solo el parámetro
- Se llama cuando data es null (solo si ni
Diferencias clave con CommandBuilder:
| Característica | CommandBuilder | toWidget() |
|---|---|---|
| BuildContext en builders | ✅ Sí (como parámetro) | ❌️ No (acceso desde build envolvente) |
| Requiere CommandResult | ❌️ No (toma Command) | ✅ Sí |
| Caso de uso | Uso directo de Command | Ya observando results |
| Precedencia de builders | Igual que toWidget() | Igual que CommandBuilder |
Cuándo Usar Qué
Usa CommandBuilder cuando:
- Construyes UI directamente desde un Command
- Prefieres composición declarativa de widgets
- No usas gestión de estado que expone results
- Quieres BuildContext pasado a las funciones builder
Usa toWidget() cuando:
- Ya observas
command.resultsvia watch_it/provider/hooks - Quieres firmas de builder más simples (sin parámetro BuildContext)
- Prefieres menos boilerplate cuando ya estás suscrito a results
Usa ValueListenableBuilder cuando:
- Necesitas control completo sobre la lógica de renderizado
- Combinaciones de estado complejas más allá de patrones estándar
- Lógica de caching personalizada crítica para rendimiento
Patrones Comunes
Loading con Datos Anteriores
Mostrar datos obsoletos mientras se cargan datos frescos:
Configuración Requerida
Este patrón requiere que el command se cree con includeLastResultInCommandResults: true. Sin esta opción, lastItems siempre será null durante la ejecución. Ver Command Results - includeLastResultInCommandResults para detalles.
// El command debe crearse con esta opción:
final searchCommand = Command.createAsync<String, List<Item>>(
searchApi,
[],
includeLastResultInCommandResults: true, // Requerido para el patrón de abajo
);
CommandBuilder(
command: searchCommand,
whileRunning: (context, lastItems, query) => Column(
children: [
LinearProgressIndicator(),
if (lastItems != null)
Opacity(opacity: 0.5, child: ItemList(lastItems)),
],
),
onData: (context, items, _) => ItemList(items),
)Error con Reintento
Configuración Requerida
Para mostrar el último valor exitoso (línea 7), el command debe crearse con includeLastResultInCommandResults: true. Ver Command Results - includeLastResultInCommandResults.
onError: (context, error, lastValue, param) => Column(
children: [
Text('Error: $error'),
ElevatedButton(
onPressed: () => command(param), // Reintentar con mismo parámetro
child: Text('Reintentar'),
),
if (lastValue != null) Text('Último exitoso: $lastValue'),
],
)Builders Condicionales
No todos los builders son requeridos - proporciona solo lo que necesitas:
// Mínimo: solo mostrar datos
CommandBuilder(
command: command,
onData: (context, data, _) => Text(data),
)
// Sin indicador de carga necesario
CommandBuilder(
command: command,
onData: (context, data, _) => Text(data),
onError: (context, error, _, __) => Text('Error: $error'),
// whileRunning omitido - no muestra nada mientras carga
)Ver También
- Command Results - Entendiendo la estructura de CommandResult
- Fundamentos de Command - Creando y ejecutando commands
- Propiedades del Command - La propiedad
.results - Observando Commands con watch_it - Usando con gestión de estado reactiva