Skip to content

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:

dart
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:

dart
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:

dart
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:

dart
// ✅ 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:

dart
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

dart
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

dart
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:

  • isRunning se mantiene true para siempre
  • El indicador de carga nunca desaparece
  • El command no se ejecuta de nuevo

Diagnóstico:

Verifica si la función async completa:

dart
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

dart
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:

dart
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:

dart
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

dart
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:

dart
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:

dart
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

dart
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

dart
class GoodCallOnce extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    callOnce((context) => di<DataManager>().loadCommand());
    return SomeWidget();
  }
}

Solución 3: Debounce de llamadas rápidas

dart
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:

dart
class ManagerNoDispose {
  late final command = Command.createAsync<String, List<Data>>(
    (query) => fetchData(),
    initialValue: [],
  );

  // ❌️ Missing dispose!
}

Solución:

Siempre dispose commands en dispose() o onDispose():

dart
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:

dart
void registerWithDispose() {
  getIt.registerSingleton<ManagerWithDispose>(
    ManagerWithDispose(),
    dispose: (manager) => manager.onDispose(),
  );
}

Problemas de Integración

watch_it no encuentra el command

Síntomas:

  • watchValue lanza 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:

dart
// ❌️ 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:

dart
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 ValueListenableBuilder directamente
  • La UI no se actualiza cuando el command completa

Diagnóstico:

Error común - creando nueva instancia en cada build:

dart
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:

dart
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.data retorna 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:

dart
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:

dart
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

dart
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)

dart
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:

dart
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:

dart
// ✅ Explicit types
final commandWithTypes = Command.createAsync<String, List<Item>>(
  (query) async => await fetchData(query),
  initialValue: [],
);

¿Aún Tienes Problemas?

  1. Revisa la documentación: Cada característica de command_it tiene documentación detallada
  2. Busca issues existentes: Issues de GitHub de command_it
  3. Pregunta en Discord: Discord de flutter_it
  4. 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

Publicado bajo la Licencia MIT.