Skip to content

Troubleshooting

Common issues with command_it and how to solve them.

Problem → Diagnosis → Solution

This guide is organized by symptoms you observe. Find your issue, diagnose the cause, and apply the solution.

UI Not Updating

Command completes but UI doesn't rebuild

Symptoms:

  • Command executes but UI doesn't update
  • Data seems unchanged
  • No errors visible

Diagnosis 1: Command threw an exception

The command might have failed silently. Check if you're listening to errors:

dart
class ManagerNoErrorHandling {
  // ❌️ No error handling - failures are invisible
  late final loadCommand = Command.createAsyncNoParam<Data>(
    () => api.fetchData().then((list) => list.first),
    initialValue: Data.empty(),
  );
}

Solution: Listen to errors or check .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());
  }
}

Diagnosis 2: Not watching the command at all

Check if you're actually observing the command's value:

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');
  }
}

Solution: Use ValueListenableBuilder or 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');
  }
}

See also: Error Handling, watch_it documentation


Command Execution Issues

Command doesn't execute / nothing happens

Symptoms:

  • Calling command('param') does nothing
  • No loading state, no errors, no results

Diagnosis:

Check if command is restricted:

dart
final someValueNotifier = ValueNotifier(true);

final restrictedCommand = Command.createAsync<String, List<Data>>(
  (query) => api.fetchData(),
  initialValue: [],
  restriction: someValueNotifier, // Is this true?
);

Solution 1: Check restriction value

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
}

Solution 2: Handle restricted execution

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
}

See also: Command Properties - Restrictions


Command stuck in "running" state

Symptoms:

  • isRunning stays true forever
  • Loading indicator never disappears
  • Command won't execute again

Diagnosis:

Check if async function completes:

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

Cause: Async function never completes

dart
final neverCompletes = Command.createAsync<String, void>((param) async {
  // ❌️ Waiting for something that never happens
  await Completer<void>().future;
}, initialValue: null);

Solution:

Add a timeout to catch hanging operations:

dart
Future<List<Data>> fetchData() => api.fetchData();

final commandWithTimeout =
    Command.createAsync<String, List<Data>>((param) async {
  return await fetchData().timeout(Duration(seconds: 30));
}, initialValue: []);

Errors Don't Cause Stuck State

If your async function throws an exception, the command catches it and resets isRunning to false. Errors won't cause a stuck running state - only futures that never complete will.


Error Handling Issues

Errors not showing in UI

Symptoms:

  • Command fails but UI doesn't show error state
  • Errors logged to crash reporter but not displayed in UI

Diagnosis:

Check if error filter only routes to global handler:

dart
final commandGlobalOnly = Command.createAsync<String, List<Data>>(
  (query) => fetchData(),
  initialValue: [],
  errorFilter: const GlobalErrorFilter(), // ❌️ UI won't see errors!
);

With globalHandler, errors go to Command.globalExceptionHandler but .errors and .results listeners are not notified.

Solution: Use a filter that includes local handler

dart
final commandLocalFilter = Command.createAsync<String, List<Data>>(
  (query) => fetchData(),
  initialValue: [],
  errorFilter: const LocalErrorFilter(), // ✅ Notifies .errors property
  // Or: const LocalAndGlobalErrorFilter() for both
);

See also: Error Handling - Error Filters


Performance Issues

Too many rebuilds / UI laggy

Symptoms:

  • UI rebuilds on every command execution
  • Even when result is identical

Diagnosis:

By default, commands notify listeners on every successful execution, even if the result is identical. This is intentional - a non-updating UI after a refresh action is often more confusing to users.

Solution: Use notifyOnlyWhenValueChanges: true

If your command frequently returns identical results and rebuilds are causing performance issues:

dart
class ItemManager {
  late final loadCommand = Command.createAsyncNoParam<List<Item>>(
    () => api.fetchItems(),
    initialValue: [],
    notifyOnlyWhenValueChanges:
        true, // ✅ Only notify when data actually changes
  );
}

When to Use This

Use notifyOnlyWhenValueChanges: true for polling/refresh commands where identical results are common. Keep the default (false) for user-triggered actions where feedback is expected.


Command executes too often

Symptoms:

  • Command runs multiple times unexpectedly
  • Seeing duplicate API calls
  • Wasting resources

Diagnosis:

Check if you're calling the command in build:

dart
class BadCallInBuild extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    command('query'); // ❌️ Called on every build!
    return SomeWidget();
  }
}

Solution 1: Call in event handlers only

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'),
    );
  }
}

Solution 2: Use callOnce for initialization

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

Solution 3: Debounce rapid calls

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

Commands not being disposed

Symptoms:

  • Memory usage grows over time
  • Flutter DevTools shows increasing listeners
  • App becomes sluggish

Diagnosis:

Check if you're disposing commands:

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

  // ❌️ Missing dispose!
}

Solution:

Always dispose commands in dispose() or onDispose():

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

  @override
  void onDispose() {
    command.dispose(); // ✅ Clean up
  }
}

For get_it singletons:

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

Integration Issues

watch_it not finding command

Symptoms:

  • watchValue throws error: "No registered instance found"
  • Command works with direct access but not with watch_it

Diagnosis:

Check if manager is registered in 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');
  }
}

Solution:

Register manager in get_it before using watch_it:

dart
void main() {
  GetIt.I.registerSingleton<DataManager>(DataManager()); // ✅ Register first
  runApp(MyApp());
}

See also: get_it documentation


ValueListenableBuilder not updating

Symptoms:

  • Using ValueListenableBuilder directly
  • UI doesn't update when command completes

Diagnosis:

Common mistake - creating new instance on every 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'),
    );
  }
}

Solution:

Command must be created once and reused:

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'),
    );
  }
}

Type Issues

CommandResult doesn't have data during loading/error

Symptoms:

  • Accessing result.data returns null unexpectedly
  • Data disappears while command is running
  • Previous data gone after an error

Diagnosis:

By default, CommandResult.data is only available after successful completion. During loading or after an error, .data is 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!
  }
}

Solution 1: Use includeLastResultInCommandResults: true

This preserves the last successful result during loading and error states:

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>

Solution 2: Check state before accessing 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
  }
}

Solution 3: Use the command directly (always has 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
  }
}

Generic type inference fails

Symptoms:

  • Dart can't infer command types
  • Need to specify types explicitly everywhere

Diagnosis:

Command created without explicit types:

dart
final commandNoTypes = Command.createAsync(
  // ❌️ Dart can't infer types from context
  (param) async => await fetchData(param as String),
  initialValue: <Item>[],
);

Solution:

Specify generic types explicitly:

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

Still Having Issues?

  1. Check the documentation: Each command_it feature has detailed documentation
  2. Search existing issues: command_it GitHub issues
  3. Ask on Discord: flutter_it Discord
  4. Create an issue: Include minimal reproduction code

When reporting issues, include:

  • Minimal code example that reproduces the problem
  • Expected behavior vs actual behavior
  • command_it version (pubspec.yaml)
  • Flutter version (flutter --version)
  • Any error messages or stack traces

Released under the MIT License.