Debugging & Troubleshooting
Common errors, solutions, debugging techniques, and troubleshooting strategies for watch_it.
Common Errors
"Watch ordering violation detected!"
Error message:
Watch ordering violation detected!
You have conditional watch calls (inside if/switch statements) that are
causing `watch_it` to retrieve the wrong objects on rebuild.
Fix: Move ALL conditional watch calls to the END of your build method.
Only the LAST watch call can be conditional.Cause: Watch calls inside if statements followed by other watches, causing order to change between builds.
Solution: See Watch Ordering Rules for detailed explanation, examples, and safe patterns.
Debugging tip: Call enableTracing() in your build method to see exact source locations of conflicting watch statements.
"watch() called outside build"
Error message:
watch() can only be called inside build()Cause: Trying to use watch functions in callbacks, constructors, or other methods.
Example:
// BAD
class WatchOutsideBuildBad extends WatchingWidget {
WatchOutsideBuildBad() {
final data = watchValue((Manager m) => m.data); // Wrong context!
}
void onPressed() {
final data = watchValue((Manager m) => m.data); // Wrong!
}
@override
Widget build(BuildContext context) {
return Container();
}
}Solution: Only call watch functions directly in build():
// GOOD
class WatchOutsideBuildGood extends WatchingWidget {
@override
Widget build(BuildContext context) {
final data = watchValue((Manager m) => m.data); // Correct!
return ElevatedButton(
onPressed: () {
doSomething(data); // Use the value
},
child: Text('$data'),
);
}
}
void doSomething(String data) {}"Type 'X' is not a subtype of type 'Listenable'"
Error message:
type 'MyManager' is not a subtype of type 'Listenable'Cause: Using watchIt<T>() on an object that isn't a Listenable.
Example:
// BAD
class TodoManagerNotListenable {
// Not a Listenable!
final todos = ValueNotifier<List<Todo>>([]);
}
class NotListenableBad extends WatchingWidget {
@override
Widget build(BuildContext context) {
// ignore: type_argument_not_matching_bounds
final manager = watchIt<TodoManagerNotListenable>(); // ERROR!
return Container();
}
}Solution: Use watchValue() instead:
// GOOD
class NotListenableGoodWatchValue extends WatchingWidget {
@override
Widget build(BuildContext context) {
final todos = watchValue((TodoManagerNotListenable m) => m.todos);
return Container();
}
}Or make your manager extend ChangeNotifier:
// Also GOOD
class TodoManagerListenable extends ChangeNotifier {
List<Todo> _todos = [];
void addTodo(Todo todo) {
_todos.add(todo);
notifyListeners(); // Now it's a Listenable
}
}
class NotListenableGoodChangeNotifier extends WatchingWidget {
@override
Widget build(BuildContext context) {
final manager = watchIt<TodoManagerListenable>(); // Works now!
return Container();
}
}"get_it: Object/factory with type X is not registered"
Error message:
get_it: Object/factory with type TodoManager is not registered inside GetItCause: Trying to watch an object that hasn't been registered in get_it.
Solution: Register it before using:
void main() {
// Register BEFORE runApp
di.registerSingleton<TodoManager>(TodoManager(DataService(ApiClient())));
runApp(MyApp());
}See get_it Object Registration for all registration methods.
Widget doesn't rebuild when data changes
Symptoms:
- Data changes but UI doesn't update
print()shows new values but widget still shows old data
Common causes:
1. Not watching the data
class NotWatchingBad extends WatchingWidget {
@override
Widget build(BuildContext context) {
// BAD - Not watching, just accessing
final manager = di<TodoManager>();
final todos = manager.todos.value; // No watch!
return Container();
}
}class NotWatchingGood extends WatchingWidget {
@override
Widget build(BuildContext context) {
// GOOD - Actually watching
final todos = watchValue((TodoManager m) => m.todos);
return Container();
}
}2. Not notifying changes
// BAD - Changing value without notifying
class TodoManagerNotNotifying {
final todos = ValueNotifier<List<Todo>>([]);
void addTodo(Todo todo) {
todos.value.add(todo); // Modifies list but doesn't notify!
}
}Option 1 - Use ListNotifier from listen_it (recommended):
// GOOD - ListNotifier automatically notifies on mutations
class TodoManagerListNotifier {
final todos = ListNotifier<Todo>(data: []);
void addTodo(Todo todo) {
todos.add(todo); // Automatically notifies listeners!
}
}Option 2 - Use custom ValueNotifier with manual notification:
// GOOD - Extend ValueNotifier and call notifyListeners
class TodoManagerCustomNotifier extends ValueNotifier<List<Todo>> {
TodoManagerCustomNotifier() : super([]);
void addTodo(Todo todo) {
value.add(todo);
notifyListeners(); // Manually trigger notification
}
}See listen_it Collections for ListNotifier, MapNotifier, and SetNotifier.
Memory leaks - subscriptions not cleaned up
Symptoms:
- Memory usage grows over time
- Old widgets still reacting to changes
- Performance degrades
Cause: Not using WatchingWidget or WatchItMixin - doing manual subscriptions.
Solution: Always use watch_it widgets:
// BAD - Manual subscriptions leak
class MemoryLeakBad extends StatelessWidget {
@override
Widget build(BuildContext context) {
final manager = di<Manager>();
manager.data.addListener(() {
// This leaks! No cleanup
});
return Container();
}
}// GOOD - Automatic cleanup
class MemoryLeakGood extends WatchingWidget {
@override
Widget build(BuildContext context) {
final data = watchValue((Manager m) => m.data);
return Text('$data');
}
}registerHandler not firing
Symptoms:
- Handler callback never executes
- Side effects (navigation, dialogs) don't happen
- No errors thrown
Common causes:
1. Handler registered after conditional return
// BAD - Handler registered AFTER early return
class HandlerAfterReturnBad extends WatchingWidget {
@override
Widget build(BuildContext context) {
final isLoading = watchValue((SaveManager m) => m.isLoading);
if (isLoading) {
return CircularProgressIndicator(); // Returns early!
}
// This handler never gets registered when loading!
registerHandler(
select: (SaveManager m) => m.saveCommand,
handler: (context, result, cancel) {
Navigator.pop(context);
},
);
return MyForm();
}
}Solution: Register handlers BEFORE any conditional returns:
// GOOD - Handler registered before conditional logic
class HandlerBeforeReturnGood extends WatchingWidget {
@override
Widget build(BuildContext context) {
registerHandler(
select: (SaveManager m) => m.saveCommand,
handler: (context, result, cancel) {
Navigator.pop(context);
},
);
final isLoading = watchValue((SaveManager m) => m.isLoading);
if (isLoading) {
return CircularProgressIndicator();
}
return MyForm();
}
}2. Widget destroyed during command execution
If the widget containing the handler is destroyed and rebuilt while the command is running, the handler will be re-registered and may miss state changes.
Example: A button inside a widget that rebuilds on hover:
// ❌ BAD: Handler inside widget that gets destroyed on parent rebuild
class ParentWidget extends StatefulWidget {
const ParentWidget({super.key});
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: Container(
color: _isHovered ? Colors.blue.shade100 : Colors.white,
child: SaveButtonWithHandler(), // ❌ Destroyed on every hover!
),
);
}
}
class SaveButtonWithHandler extends WatchingWidget {
@override
Widget build(BuildContext context) {
// ❌ Handler is registered here - if parent rebuilds,
// this widget is destroyed and handler is lost!
registerHandler(
select: (TodoManager m) => m.createTodoCommand.results,
handler: (context, result, cancel) {
if (result.hasData) {
Navigator.of(context).pop(); // May never execute!
}
},
);
return ElevatedButton(
onPressed: () => di<TodoManager>().createTodoCommand(
CreateTodoParams(title: 'New', description: 'Task'),
),
child: const Text('Save'),
);
}
}Solution: Move the handler to a stable parent widget:
// ✅ GOOD: Handler in stable parent widget
class StableParentWidget extends WatchingStatefulWidget {
const StableParentWidget({super.key});
@override
State<StableParentWidget> createState() => _StableParentWidgetState();
}
class _StableParentWidgetState extends State<StableParentWidget> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
// ✅ Handler registered in parent - survives child rebuilds
registerHandler(
select: (TodoManager m) => m.createTodoCommand.results,
handler: (context, result, cancel) {
if (result.hasData) {
Navigator.of(context).pop(); // Always executes!
}
},
);
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: Container(
color: _isHovered ? Colors.blue.shade100 : Colors.white,
child: const SaveButtonOnly(), // Child can rebuild safely
),
);
}
}
class SaveButtonOnly extends StatelessWidget {
const SaveButtonOnly({super.key});
@override
Widget build(BuildContext context) {
// Just the button - no handler here
return ElevatedButton(
onPressed: () => di<TodoManager>().createTodoCommand(
CreateTodoParams(title: 'New', description: 'Task'),
),
child: const Text('Save'),
);
}
}Debugging Techniques
Enable watch_it Tracing
Get detailed logs of watch subscriptions and source locations for ordering violations:
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Call at the start of build to enable tracing
enableTracing(
logRebuilds: true,
logHandlers: true,
logHelperFunctions: true,
);
final todos = watchValue((TodoManager m) => m.todos);
// ... rest of build
}
}Benefits:
- Shows which watch triggered the rebuild of your widget
- Shows exact source locations of watch calls
- Helps identify ordering violations
- Tracks rebuild activity
- Shows handler executions
Use case: When your widget rebuilds unexpectedly, enable tracing to see exactly which watched value changed and triggered the rebuild. This helps you identify if you're watching too much data or the wrong properties.
Alternative: Use WatchItSubTreeTraceControl widget to enable tracing for a specific subtree:
// First, enable subtree tracing globally (typically in main())
enableSubTreeTracing = true;
// Then wrap ONLY the problematic widget/screen - NOT the whole app!
// Otherwise you'll drown in logs from every widget
return Scaffold(
body: WatchItSubTreeTraceControl(
logRebuilds: true, // Required: log rebuild events
logHandlers: true, // Required: log handler executions
logHelperFunctions: true, // Required: log helper function calls
child: ProblematicWidget(), // Only the widget you're debugging
),
);Important: Wrap only the specific widget or screen causing issues, not your entire app. Tracing the whole app generates overwhelming amounts of logs.
Note:
- You can nest multiple
WatchItSubTreeTraceControlwidgets - the nearest ancestor's settings apply - Must set
enableSubTreeTracing = trueglobally for subtree controls to work
Isolate the problem
Create minimal reproduction:
class IsolateProblem extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Minimal example - just the watch and rebuild
final value = watchValue((CounterManager m) => m.count);
print('Rebuild with value: $value');
return Text('Value: $value');
}
}This isolates:
- Does the watch subscription work?
- Does the widget rebuild on data change?
- Are there ordering issues?
Getting Help
When reporting issues:
- Minimal reproduction - Isolate the problem
- Versions -
watch_it, Flutter, Dart versions - Error messages - Full stack trace
- Expected vs actual - What should happen vs what happens
- Code sample - Complete, runnable example
Where to ask:
- Discord: Join flutter_it community
- GitHub Issues: watch_it issues
- Stack Overflow: Tag with
flutterandwatch-it
See Also
- Watch Ordering Rules - CRITICAL constraints
- Best Practices - Patterns and tips
- How watch_it Works - Understanding the mechanism