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:
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:
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:
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:
// ✅ 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:
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
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
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:
isRunningstaystrueforever- Loading indicator never disappears
- Command won't execute again
Diagnosis:
Check if async function completes:
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
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:
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:
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
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:
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:
class BadCallInBuild extends WatchingWidget {
@override
Widget build(BuildContext context) {
command('query'); // ❌️ Called on every build!
return SomeWidget();
}
}Solution 1: Call in event handlers only
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
class GoodCallOnce extends WatchingWidget {
@override
Widget build(BuildContext context) {
callOnce((context) => di<DataManager>().loadCommand());
return SomeWidget();
}
}Solution 3: Debounce rapid calls
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:
class ManagerNoDispose {
late final command = Command.createAsync<String, List<Data>>(
(query) => fetchData(),
initialValue: [],
);
// ❌️ Missing dispose!
}Solution:
Always dispose commands in dispose() or onDispose():
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:
void registerWithDispose() {
getIt.registerSingleton<ManagerWithDispose>(
ManagerWithDispose(),
dispose: (manager) => manager.onDispose(),
);
}Integration Issues
watch_it not finding command
Symptoms:
watchValuethrows error: "No registered instance found"- Command works with direct access but not with
watch_it
Diagnosis:
Check if manager is registered in 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');
}
}Solution:
Register manager in get_it before using watch_it:
void main() {
GetIt.I.registerSingleton<DataManager>(DataManager()); // ✅ Register first
runApp(MyApp());
}See also: get_it documentation
ValueListenableBuilder not updating
Symptoms:
- Using
ValueListenableBuilderdirectly - UI doesn't update when command completes
Diagnosis:
Common mistake - creating new instance on every 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'),
);
}
}Solution:
Command must be created once and reused:
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.datareturns 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:
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:
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
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)
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:
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:
// ✅ Explicit types
final commandWithTypes = Command.createAsync<String, List<Item>>(
(query) async => await fetchData(query),
initialValue: [],
);Still Having Issues?
- Check the documentation: Each command_it feature has detailed documentation
- Search existing issues: command_it GitHub issues
- Ask on Discord: flutter_it Discord
- 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