Resolución de Problemas
Problemas comunes con command_it y cómo solucionarlos.
Problema → Diagnóstico → Solución
Esta guía está organizada por síntomas que observas. Encuentra tu problema, diagnostica la causa, y aplica la solución.
UI No Se Actualiza
El command completa pero la UI no se reconstruye
Síntomas:
- El command se ejecuta pero la UI no se actualiza
- Los datos parecen sin cambios
- No hay errores visibles
Diagnóstico 1: El command lanzó una excepción
El command podría haber fallado silenciosamente. Verifica si estás escuchando errores:
class ManagerNoErrorHandling {
// ❌️ No error handling - failures are invisible
late final loadCommand = Command.createAsyncNoParam<Data>(
() => api.fetchData().then((list) => list.first),
initialValue: Data.empty(),
);
}Solución: Escucha errores o verifica .results:
class ManagerWithErrorHandling {
// ✅ Option 1: Listen to errors on command definition
late final loadCommand = Command.createAsyncNoParam<Data>(
() => api.fetchData().then((list) => list.first),
initialValue: Data.empty(),
)..errors.listen((error, _) {
if (error != null) debugPrint('Load failed: ${error.error}');
});
}
class WidgetWatchingResults extends WatchingWidget {
@override
Widget build(BuildContext context) {
// ✅ Option 2: Watch .results in UI to see all states
final result =
watchValue((ManagerWithErrorHandling m) => m.loadCommand.results);
if (result.hasError) return ErrorWidget(result.error!);
return Text(result.data.toString());
}
}Diagnóstico 2: No estás observando el command en absoluto
Verifica si realmente estás observando el valor del command:
class BadStaticRead extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ❌️ Reading value once - won't update when command completes
final data = di<ManagerWithErrorHandling>()
.loadCommand
.value; // Static read, no subscription!
return Text('$data');
}
}Solución: Usa ValueListenableBuilder o watch_it:
// ✅ Option 1: ValueListenableBuilder
class GoodValueListenableBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: di<ManagerWithErrorHandling>().loadCommand,
builder: (context, data, _) => Text('$data'),
);
}
}
// ✅ Option 2: watch_it (requires WatchingWidget)
class GoodWatchIt extends WatchingWidget {
@override
Widget build(BuildContext context) {
final data = watchValue((ManagerWithErrorHandling m) => m.loadCommand);
return Text('$data');
}
}Ver también: Manejo de Errores (Error Handling), documentación de watch_it
Problemas de Ejecución de Commands
El command no se ejecuta / no pasa nada
Síntomas:
- Llamar
command('param')no hace nada - Sin estado de carga, sin errores, sin resultados
Diagnóstico:
Verifica si el command está restringido:
final someValueNotifier = ValueNotifier(true);
final restrictedCommand = Command.createAsync<String, List<Data>>(
(query) => api.fetchData(),
initialValue: [],
restriction: someValueNotifier, // Is this true?
);Solución 1: Verifica el valor de la restricción
void debugRestriction() {
final restriction = ValueNotifier(true);
final command = Command.createAsync<String, List<Data>>(
(query) => api.fetchData(),
initialValue: [],
restriction: restriction,
);
// Debug: print restriction state
print('Can run: ${command.canRun.value}');
print('Is restricted: ${restriction.value}'); // Should be false to run
}Solución 2: Maneja la ejecución restringida
final isLoggedOut = ValueNotifier(false);
final commandWithHandler = Command.createAsync<String, List<Data>>(
(query) => api.fetchData(),
initialValue: [],
restriction: isLoggedOut,
ifRestrictedRunInstead: (param) {
// Show login dialog
showLoginDialog();
},
);
void showLoginDialog() {
// Implementation
}Ver también: Propiedades del Command - Restricciones
El command está atascado en estado "running"
Síntomas:
isRunningse mantienetruepara siempre- El indicador de carga nunca desaparece
- El command no se ejecuta de nuevo
Diagnóstico:
Verifica si la función async completa:
final Future<void> neverCompletingFuture = Completer<void>().future;
final stuckCommand = Command.createAsync<String, void>((param) async {
await api.fetchData(); // Does this ever complete?
// Missing return statement?
}, initialValue: null);Causa: La función async nunca completa
final neverCompletes = Command.createAsync<String, void>((param) async {
// ❌️ Waiting for something that never happens
await Completer<void>().future;
}, initialValue: null);Solución:
Agrega un timeout para capturar operaciones colgadas:
Future<List<Data>> fetchData() => api.fetchData();
final commandWithTimeout =
Command.createAsync<String, List<Data>>((param) async {
return await fetchData().timeout(Duration(seconds: 30));
}, initialValue: []);Los Errores No Causan Estado Atascado
Si tu función async lanza una excepción, el command la captura y resetea isRunning a false. Los errores no causarán un estado running atascado - solo futures que nunca completan lo harán.
Problemas de Manejo de Errores
Los errores no se muestran en UI
Síntomas:
- El command falla pero la UI no muestra estado de error
- Errores logueados a crash reporter pero no mostrados en UI
Diagnóstico:
Verifica si el filtro de error solo enruta a handler global:
final commandGlobalOnly = Command.createAsync<String, List<Data>>(
(query) => fetchData(),
initialValue: [],
errorFilter: const GlobalErrorFilter(), // ❌️ UI won't see errors!
);Con globalHandler, los errores van a Command.globalExceptionHandler pero los listeners de .errors y .results no son notificados.
Solución: Usa un filtro que incluya handler local
final commandLocalFilter = Command.createAsync<String, List<Data>>(
(query) => fetchData(),
initialValue: [],
errorFilter: const LocalErrorFilter(), // ✅ Notifies .errors property
// Or: const LocalAndGlobalErrorFilter() for both
);Ver también: Manejo de Errores (Error Handling) - Filtros de Error
Problemas de Rendimiento
Demasiados rebuilds / UI lenta
Síntomas:
- La UI se reconstruye en cada ejecución de command
- Incluso cuando el resultado es idéntico
Diagnóstico:
Por defecto, los commands notifican a listeners en cada ejecución exitosa, incluso si el resultado es idéntico. Esto es intencional - una UI que no se actualiza después de una acción de refresh a menudo es más confusa para los usuarios.
Solución: Usa notifyOnlyWhenValueChanges: true
Si tu command frecuentemente retorna resultados idénticos y los rebuilds están causando problemas de rendimiento:
class ItemManager {
late final loadCommand = Command.createAsyncNoParam<List<Item>>(
() => api.fetchItems(),
initialValue: [],
notifyOnlyWhenValueChanges:
true, // ✅ Only notify when data actually changes
);
}Cuándo Usar Esto
Usa notifyOnlyWhenValueChanges: true para commands de polling/refresh donde resultados idénticos son comunes. Mantén el valor por defecto (false) para acciones disparadas por usuario donde se espera feedback.
El command se ejecuta muy a menudo
Síntomas:
- El command se ejecuta múltiples veces inesperadamente
- Viendo llamadas API duplicadas
- Desperdiciando recursos
Diagnóstico:
Verifica si estás llamando al command en build:
class BadCallInBuild extends WatchingWidget {
@override
Widget build(BuildContext context) {
command('query'); // ❌️ Called on every build!
return SomeWidget();
}
}Solución 1: Llama solo en handlers de eventos
class GoodEventHandler extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Call command only when button is pressed
return ElevatedButton(
onPressed: () => command('query'),
child: const Text('Search'),
);
}
}Solución 2: Usa callOnce para inicialización
class GoodCallOnce extends WatchingWidget {
@override
Widget build(BuildContext context) {
callOnce((context) => di<DataManager>().loadCommand());
return SomeWidget();
}
}Solución 3: Debounce de llamadas rápidas
class SearchManager {
// In your manager
late final debouncedSearch = Command.createSync<String, String>(
(query) => query,
initialValue: '',
);
late final actualSearch = Command.createAsync<String, List<Data>>(
(query) => ApiClient().fetchData(),
initialValue: [],
);
SearchManager() {
debouncedSearch.debounce(Duration(milliseconds: 500)).listen((query, _) {
actualSearch(query);
});
}
}Memory Leaks
Los commands no se están disposing
Síntomas:
- El uso de memoria crece con el tiempo
- Flutter DevTools muestra listeners incrementándose
- La app se vuelve lenta
Diagnóstico:
Verifica si estás disposing commands:
class ManagerNoDispose {
late final command = Command.createAsync<String, List<Data>>(
(query) => fetchData(),
initialValue: [],
);
// ❌️ Missing dispose!
}Solución:
Siempre dispose commands en dispose() o onDispose():
class ManagerWithDispose with Disposable {
late final command = Command.createAsync<String, List<Data>>(
(query) => fetchData(),
initialValue: [],
);
@override
void onDispose() {
command.dispose(); // ✅ Clean up
}
}Para singletons de get_it:
void registerWithDispose() {
getIt.registerSingleton<ManagerWithDispose>(
ManagerWithDispose(),
dispose: (manager) => manager.onDispose(),
);
}Problemas de Integración
watch_it no encuentra el command
Síntomas:
watchValuelanza error: "No registered instance found"- El command funciona con acceso directo pero no con
watch_it
Diagnóstico:
Verifica si el manager está registrado en get_it:
// ❌️ Manager not registered
class WidgetWithoutRegistration extends WatchingWidget {
@override
Widget build(BuildContext context) {
final data = watchValue((DataManager m) => m.command); // Fails!
return Text('$data');
}
}Solución:
Registra el manager en get_it antes de usar watch_it:
void main() {
GetIt.I.registerSingleton<DataManager>(DataManager()); // ✅ Register first
runApp(MyApp());
}Ver también: documentación de get_it
ValueListenableBuilder no se actualiza
Síntomas:
- Usando
ValueListenableBuilderdirectamente - La UI no se actualiza cuando el command completa
Diagnóstico:
Error común - creando nueva instancia en cada build:
class BadNewInstanceEveryBuild extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ❌️ Creating new instance on every build
return ValueListenableBuilder(
valueListenable: Command.createAsync<String, List<Data>>(
(query) => fetch(),
initialValue: [],
), // New command each build!
builder: (context, value, _) => Text('$value'),
);
}
}Solución:
El command debe crearse una vez y reutilizarse:
class DataManager {
late final command = Command.createAsync<String, List<Data>>(
(query) => fetch(),
initialValue: [],
); // ✅ Created once
}
class GoodReuseInstance extends StatelessWidget {
final DataManager manager;
const GoodReuseInstance({super.key, required this.manager});
@override
Widget build(BuildContext context) {
// In widget:
return ValueListenableBuilder(
valueListenable: manager.command, // ✅ Same instance
builder: (context, value, _) => Text('$value'),
);
}
}Problemas de Tipos
CommandResult no tiene data durante loading/error
Síntomas:
- Acceder a
result.dataretorna null inesperadamente - Los datos desaparecen mientras el command se ejecuta
- Datos anteriores desaparecen después de un error
Diagnóstico:
Por defecto, CommandResult.data solo está disponible después de completación exitosa. Durante loading o después de un error, .data es null:
class DiagnosisWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final result = watchValue((DataManager m) => m.loadCommand.results);
// During loading: result.isRunning = true, result.data = null
// After error: result.hasError = true, result.data = null
// After success: result.hasData = true, result.data = <your data>
return Text(result.data.toString()); // ❌ Crashes during loading/error!
}
}Solución 1: Usa includeLastResultInCommandResults: true
Esto preserva el último resultado exitoso durante estados de loading y error:
class ManagerWithLastResult {
late final loadCommand = Command.createAsyncNoParam<List<Item>>(
() => api.fetchItems(),
initialValue: [],
includeLastResultInCommandResults: true, // ✅ Keep old data visible
);
}
// Now in your widget:
// During loading: result.data = <previous successful data>
// After error: result.data = <previous successful data>
// After success: result.data = <new data>Solución 2: Verifica estado antes de acceder a data
class Solution2Widget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final result = watchValue((DataManager m) => m.loadCommand.results);
if (result.isRunning) return CircularProgressIndicator();
if (result.hasError) return ErrorWidget(result.error!);
return DataWidget(result.data!); // ✅ Safe - hasData is true
}
}Solución 3: Usa el command directamente (siempre tiene data)
class Solution3Widget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Command's value always has data (uses initialValue as fallback)
final data = watchValue((DataManager m) => m.loadCommand);
return DataWidget(data); // ✅ Always has a value
}
}La inferencia de tipos genéricos falla
Síntomas:
- Dart no puede inferir tipos de commands
- Necesitas especificar tipos explícitamente en todas partes
Diagnóstico:
Command creado sin tipos explícitos:
final commandNoTypes = Command.createAsync(
// ❌️ Dart can't infer types from context
(param) async => await fetchData(param as String),
initialValue: <Item>[],
);Solución:
Especifica tipos genéricos explícitamente:
// ✅ Explicit types
final commandWithTypes = Command.createAsync<String, List<Item>>(
(query) async => await fetchData(query),
initialValue: [],
);¿Aún Tienes Problemas?
- Revisa la documentación: Cada característica de command_it tiene documentación detallada
- Busca issues existentes: Issues de GitHub de command_it
- Pregunta en Discord: Discord de flutter_it
- Crea un issue: Incluye código mínimo de reproducción
Al reportar issues, incluye:
- Ejemplo de código mínimo que reproduce el problema
- Comportamiento esperado vs comportamiento actual
- Versión de command_it (
pubspec.yaml) - Versión de Flutter (
flutter --version) - Cualquier mensaje de error o stack traces