Command Results
Inmersión profunda en CommandResult - el objeto de estado comprehensivo que combina estado de ejecución, datos de resultado, errores y parámetros en una sola propiedad observable.
Resumen
La propiedad .results es un ValueListenable<CommandResult<TParam, TResult>> que proporciona toda la información de ejecución del command en una sola clase de valor. Esta propiedad se actualiza en cada cambio de estado del command (running, éxito, error):
class CommandResult<TParam, TResult> {
final TParam? paramData; // Parámetro pasado al command
final TResult? data; // Valor del resultado
final bool isUndoValue; // True si es de una operación de undo
final Object? error; // Error si se lanzó
final bool isRunning; // Estado de ejecución
final ErrorReaction? errorReaction; // Cómo se manejó el error (si ocurrió)
final StackTrace? stackTrace; // Stack trace del error (si ocurrió)
// Getters de conveniencia
bool get hasData => data != null;
bool get hasError => error != null && !isUndoValue; // Excluye errores de undo
bool get isSuccess => !isRunning && !hasError;
}Acceso via la propiedad .results:
ValueListenableBuilder<CommandResult<String, List<Todo>>>(
valueListenable: command.results,
builder: (context, result, _) {
// Usa result.data, result.error, result.isRunning, etc.
},
)Cuándo Usar CommandResult
Usa .results cuando necesites:
- ✅ Todo el estado en un solo lugar (running, data, error)
- ✅ Datos del parámetro para mensajes de error
- ✅ Un solo builder en lugar de múltiples builders anidados
- ✅ Manejo de estado comprehensivo
Usa propiedades individuales cuando:
- Solo necesitas los datos: Usa el command mismo (
ValueListenable<TResult>) - Solo necesitas el estado de carga: Usa `.isRunning`
- Solo necesitas errores: Usa `.errors`
- Quieres evitar rebuilds en cada cambio de estado (las propiedades individuales solo se actualizan para su estado específico)
Transiciones de Estado del Result
Flujo Normal (Éxito)
Inicial: { data: null, error: null, isRunning: false }
↓ command.run('query')
Running: { data: null, error: null, isRunning: true }
↓ operación async completa
Éxito: { data: [results], error: null, isRunning: false }Nota: El data inicial es null a menos que establezcas un parámetro initialValue al crear el command.
Flujo de Error
Inicial: { data: null, error: null, isRunning: false }
↓ command.run('query')
Running: { data: null, error: null, isRunning: true }
↓ se lanza excepción
Error: { data: null, error: Exception(), isRunning: false }includeLastResultInCommandResults
Por defecto, CommandResult.data se vuelve null durante la ejecución del command y cuando ocurren errores. Establece includeLastResultInCommandResults: true para mantener el último valor exitoso visible en ambos estados:
Command.createAsync<String, List<Todo>>(
(query) => api.search(query),
initialValue: [],
includeLastResultInCommandResults: true, // Mantener datos antiguos visibles
);Cuándo este flag afecta el comportamiento:
- Durante la ejecución (
isRunning: true) - Los datos antiguos permanecen enresult.dataen lugar de volversenull - Durante estados de error (
hasError: true) - Los datos antiguos permanecen enresult.dataen lugar de volversenull
Flujo modificado (con initialValue: []):
Inicial: { data: [], error: null, isRunning: false }
↓ command.run('query')
Running: { data: [], error: null, isRunning: true } ← Datos antiguos mantenidos
↓ éxito
Éxito: { data: [nuevos resultados], error: null, isRunning: false }
↓ command.run('query2')
Running: { data: [resultados antiguos], error: null, isRunning: true } ← Aún visibles
↓ error
Error: { data: [resultados antiguos], error: Exception(), isRunning: false } ← Aún visiblesCasos de uso comunes:
- Pull-to-refresh - Mostrar datos obsoletos mientras se cargan datos frescos
- Stale-while-revalidate - Seguir mostrando contenido antiguo durante actualizaciones
- Recuperación de errores - Mostrar últimos datos buenos conocidos incluso cuando ocurren errores
- UI Optimista - Mantener estabilidad de UI durante refrescos en background
Cuándo usar:
- ✅ Escenarios de refresco de listas/feeds donde estados vacíos se ven mal
- ✅ Resultados de búsqueda que se actualizan incrementalmente
- ✅ Datos que es mejor tener obsoletos que ausentes
- ❌️ Login/autenticación donde datos obsoletos son engañosos
- ❌️ Datos críticos donde mostrar valores antiguos durante errores es inseguro
Ejemplo Completo
Con watch_it (Recomendado)
class WeatherResultWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Watch the results property for all state
final results = watchValue(
(WeatherManager m) => m.loadWeatherCommand.results,
);
// Check execution state
if (results.isRunning) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading weather for ${results.paramData ?? ""}...'),
],
),
);
}
// Check for errors
if (results.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, color: Colors.red, size: 48),
SizedBox(height: 16),
Text('Error: ${results.error}'),
if (results.paramData != null)
Text('For city: ${results.paramData}'),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => weatherManager.loadWeatherCommand('London'),
child: Text('Retry'),
),
],
),
);
}
// Check for data
if (results.hasData && results.data!.isNotEmpty) {
return ListView.builder(
itemCount: results.data!.length,
itemBuilder: (context, index) {
final entry = results.data![index];
return ListTile(
title: Text(entry.city),
subtitle: Text(entry.condition),
trailing: Text('${entry.temperature}°F'),
);
},
);
}
// Initial state
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => weatherManager.loadWeatherCommand('London'),
child: Text('Load Weather'),
),
SizedBox(height: 8),
ElevatedButton(
onPressed: () {
weatherManager.shouldFail = true;
weatherManager.loadWeatherCommand('Paris');
},
child: Text('Load Weather (will fail)'),
),
],
),
);
}
}Cómo funciona:
watchValueobserva la propiedad.results- El widget se reconstruye automáticamente cuando cambia el estado
- Verifica
result.isRunningprimero → mostrar carga - Verifica
result.hasErrordespués → mostrar error (con datos del parámetro) - Verifica
result.hasData→ mostrar datos - Fallback → estado inicial
Sin watch_it
class WeatherResultWidget extends StatelessWidget {
WeatherResultWidget({super.key});
final manager = WeatherManager();
@override
Widget build(BuildContext context) {
// Use results property for all data at once
return ValueListenableBuilder<CommandResult<String?, List<WeatherEntry>>>(
valueListenable: manager.loadWeatherCommand.results,
builder: (context, result, _) {
// Check execution state
if (result.isRunning) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading weather for ${result.paramData ?? ""}...'),
],
),
);
}
// Check for errors
if (result.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, color: Colors.red, size: 48),
SizedBox(height: 16),
Text('Error: ${result.error}'),
if (result.paramData != null)
Text('For city: ${result.paramData}'),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => manager.loadWeatherCommand('London'),
child: Text('Retry'),
),
],
),
);
}
// Check for data
if (result.hasData && result.data!.isNotEmpty) {
return ListView.builder(
itemCount: result.data!.length,
itemBuilder: (context, index) {
final entry = result.data![index];
return ListTile(
title: Text(entry.city),
subtitle: Text(entry.condition),
trailing: Text('${entry.temperature}°F'),
);
},
);
}
// Initial state
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => manager.loadWeatherCommand('London'),
child: Text('Load Weather'),
),
SizedBox(height: 8),
ElevatedButton(
onPressed: () {
manager.shouldFail = true;
manager.loadWeatherCommand('Paris');
},
child: Text('Load Weather (will fail)'),
),
],
),
);
},
);
}
}La misma lógica usando ValueListenableBuilder para usuarios que prefieren no usar watch_it.
Usando .toWidget() con CommandResult
El método de extensión .toWidget() proporciona una forma declarativa de construir UI desde CommandResult. Para documentación completa sobre cómo usar .toWidget(), incluyendo:
- Parámetros del builder y reglas de precedencia
- Diferencias entre
onDatayonSuccess - Cuándo usar
.toWidget()vs verificaciones manuales de estado - Ejemplos y patrones comunes
Ver Command Builders - Método de Extensión toWidget()
Propiedades del Result
data - El Valor del Resultado
if (result.hasData) {
final items = result.data!; // Seguro de desempaquetar
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) => ItemTile(items[i]),
);
}Comportamiento:
nullmientras el command se ejecuta (a menos queincludeLastResultInCommandResults)nullen error (a menos queincludeLastResultInCommandResults)- Contiene el valor del resultado en éxito
- Siempre
nullpara commands con resultadovoid
Nullability:
- El tipo es
TResult?(nullable) - Usa
hasDatapara verificar antes de acceder - Seguro de desempaquetar después de verificar
hasData
error - La Excepción
if (result.hasError) {
return ErrorWidget(
message: result.error.toString(),
onRetry: command.run,
);
}Comportamiento:
nullcuando no hay error- Contiene la excepción lanzada en fallo
- Se limpia a
nullcuando el command se ejecuta de nuevo - El tipo es
Object?(cualquier cosa lanzable)
CommandResult.error vs Propiedad Command.errors
Distinción importante:
CommandResult.errorcontiene el objeto de error puro/raw (tipoObject?)- La propiedad
.errorsdel command contieneCommandError<TParam>?que envuelve el error con contexto adicional (datos del parámetro, nombre del command, stack trace, reacción al error)
Cuando usas CommandResult, tienes acceso directo al error lanzado. Cuando usas la propiedad .errors, obtienes el error envuelto con metadata.
Tipos de error:
if (result.hasError) {
if (result.error is ApiException) {
// Manejar errores de API
} else if (result.error is ValidationException) {
// Manejar errores de validación
} else {
// Error genérico
}
}Manejo de Errores (Error Handling) en UI vs Filtros de Error
El patrón anterior es recomendado para mostrar diferente UI basándose en tipo de error. Para estrategias de manejo de errores más sofisticadas (enrutar errores a diferentes handlers, logging, relanzar, silenciar errores específicos, etc.), usa Filtros de Error que ofrecen posibilidades mucho más ricas para controlar reacciones a errores.
isRunning - Estado de Ejecución
if (result.isRunning) {
return Center(
child: Column(
children: [
CircularProgressIndicator(),
Text('Cargando...'),
],
),
);
}Comportamiento:
truemientras la función async se ejecutafalseinicialmente y después de completar- Se actualiza asíncronamente (via microtask) - ver Propiedades del Command
paramData - El Parámetro de Entrada
if (result.hasError) {
return Column(
children: [
Text('Error: ${result.error}'),
if (result.paramData != null)
Text('Falló para la consulta: ${result.paramData}'),
ElevatedButton(
onPressed: () => command(result.paramData), // Reintentar con mismo parámetro
child: Text('Reintentar'),
),
],
);
}Comportamiento:
- Contiene el parámetro pasado al command
nullpara commands sin parámetro- El tipo es
TParam?(nullable) - Útil para mensajes de error y lógica de reintento
Casos de uso:
- Mostrar qué consulta falló en el mensaje de error
- Botón de reintentar con los mismos parámetros
- Logging de qué operación falló
Getters de Conveniencia
hasData
bool get hasData => data != null;
// Uso
if (result.hasData) {
return DataView(result.data!);
}Preferido sobre:
if (result.data != null) { ... }hasError
bool get hasError => error != null;
// Uso
if (result.hasError) {
return ErrorView(result.error.toString());
}Preferido sobre:
if (result.error != null) { ... }isSuccess
bool get isSuccess => !hasError && !isRunning;
// Uso
if (result.isSuccess && result.hasData) {
return SuccessView(result.data!);
}Útil para:
- Distinguir completación exitosa del estado inicial
- Mostrar animaciones/mensajes de éxito
- Renderizado condicional después de completar
Patrones con CommandResult
Patrón 1: Estados Progresivos
Con watch_it:
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final result = watchValue((Manager m) => m.command.results);
// 1. Loading
if (result.isRunning) {
return LoadingState(query: result.paramData);
}
// 2. Error
if (result.hasError) {
return ErrorState(
error: result.error!,
query: result.paramData,
onRetry: () => di<Manager>().command(result.paramData),
);
}
// 3. Éxito
if (result.hasData) {
return DataState(data: result.data!);
}
// 4. Inicial (sin datos, sin error, no ejecutándose)
return InitialState();
}
}Sin watch_it:
ValueListenableBuilder<CommandResult<String, Data>>(
valueListenable: command.results,
builder: (context, result, _) {
// 1. Loading
if (result.isRunning) {
return LoadingState(query: result.paramData);
}
// 2. Error
if (result.hasError) {
return ErrorState(
error: result.error!,
query: result.paramData,
onRetry: () => command(result.paramData),
);
}
// 3. Éxito
if (result.hasData) {
return DataState(data: result.data!);
}
// 4. Inicial (sin datos, sin error, no ejecutándose)
return InitialState();
},
)Patrón 2: UI Optimista con Datos Obsoletos
Setup:
Command.createAsync<String, List<Item>>(
(query) => api.search(query),
initialValue: [],
includeLastResultInCommandResults: true, // Mantener datos antiguos
);Con watch_it:
class SearchWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final result = watchValue((SearchManager m) => m.searchCommand.results);
return Stack(
children: [
// Siempre mostrar datos (antiguos o nuevos)
if (result.hasData)
ItemList(items: result.data!),
// Superponer indicador de carga
if (result.isRunning)
Positioned(
top: 0,
left: 0,
right: 0,
child: LinearProgressIndicator(),
),
// Mostrar banner de error
if (result.hasError)
ErrorBanner(error: result.error),
],
);
}
}Sin watch_it:
ValueListenableBuilder<CommandResult<String, List<Item>>>(
valueListenable: searchCommand.results,
builder: (context, result, _) {
return Stack(
children: [
// Siempre mostrar datos (antiguos o nuevos)
if (result.hasData)
ItemList(items: result.data!),
// Superponer indicador de carga
if (result.isRunning)
Positioned(
top: 0,
left: 0,
right: 0,
child: LinearProgressIndicator(),
),
// Mostrar banner de error
if (result.hasError)
ErrorBanner(error: result.error),
],
);
},
)Patrón 3: Reintentar con Parámetros Originales
if (result.hasError) {
return ErrorView(
error: result.error!,
operation: 'Buscando "${result.paramData}"',
onRetry: () {
// Reintentar con exactamente el mismo parámetro
command(result.paramData);
},
);
}Patrón 4: Logging con Contexto
Usa la propiedad .errors para logging - proporciona contexto más rico que CommandResult.error:
command.errors.listen((commandError, _) {
if (commandError != null) {
logger.error(
'Command falló: ${commandError.command}',
error: commandError.error,
stackTrace: commandError.stackTrace,
param: commandError.paramData,
errorReaction: commandError.errorReaction,
);
}
});Por qué .errors es mejor para logging:
- Incluye
stackTracecapturado automáticamente - Proporciona nombre del
commandpara identificar qué command falló - Contiene
errorReactionmostrando cómo se manejó el error - Todo el contexto empaquetado en el wrapper
CommandError<TParam>
CommandResult vs Propiedades Individuales
Usando propiedades individuales (múltiples watchers)
// Con watch_it - solo reconstruye para las propiedades que observas
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final isRunning = watchValue((TodoManager m) => m.loadTodos.isRunning);
final todos = watchValue((TodoManager m) => m.loadTodos);
if (isRunning) return CircularProgressIndicator();
return TodoList(todos: todos);
}
}Beneficios:
- Cada propiedad solo se actualiza cuando su valor cambia
- No se necesitan verificaciones
ifcuando observas 1-2 propiedades - Menos rebuilds - solo cuando las propiedades observadas cambian
Usando CommandResult (watcher único)
// Propiedad única con verificaciones if
class TodoWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final result = watchValue((TodoManager m) => m.loadTodos.results);
if (result.isRunning) return CircularProgressIndicator();
if (result.hasError) return ErrorWidget(result.error);
return TodoList(todos: result.data ?? []);
}
}Trade-offs:
- Más rebuilds: Se actualiza en cada cambio de estado (running, éxito, error)
- Requiere verificaciones
if: Debes verificar propiedades de estado - Watcher único: Todo el estado en un solo lugar
- Mejor para: Cuando necesitas 3+ propiedades o toda la información de estado
Recomendación:
- Necesitas solo 1-2 propiedades (ej., solo data + isRunning): Usa propiedades individuales
- Necesitas 3+ propiedades o estado completo: Usa CommandResult
Errores Comunes
❌️️ Acceder a data sin verificación de null
// MAL: data podría ser null
return ListView.builder(
itemCount: result.data.length, // ¡Crash si es null!
...
);// CORRECTO: Verificar hasData primero
if (result.hasData) {
return ListView.builder(
itemCount: result.data!.length,
...
);
}❌️️ Orden incorrecto de verificación de estado
// MAL: Verifica data antes de verificar isRunning
if (result.hasData) return DataView(result.data!);
if (result.isRunning) return LoadingView();// CORRECTO: Verificar isRunning primero
if (result.isRunning) return LoadingView();
if (result.hasData) return DataView(result.data!);❌️️ Ignorar el estado inicial
// MAL: ¿Qué pasa si no hay data, no hay error, no está ejecutándose?
if (result.isRunning) return LoadingView();
if (result.hasError) return ErrorView(result.error!);
return DataView(result.data!); // ¡Crash en estado inicial!// CORRECTO: Manejar todos los estados
if (result.isRunning) return LoadingView();
if (result.hasError) return ErrorView(result.error!);
if (result.hasData) return DataView(result.data!);
return InitialView(); // Estado inicialVer También
- Propiedades del Command — Todas las propiedades observables del command
- Fundamentos de Command — Creando y ejecutando commands
- Manejo de Errores (Error Handling) — Uso de la propiedad errors
- Widget CommandBuilder — Widget que usa CommandResult