This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
watch_it is a Flutter state management package built on top of get_it. It provides reactive data binding that automatically rebuilds widgets when observed data changes, eliminating the need for ValueListenableBuilder, StreamBuilder, and FutureBuilder widgets.
Core philosophy: Simple, hook-like API (similar to React Hooks/flutter_hooks) that watches registered objects in get_it and rebuilds widgets automatically.
# Run all tests
flutter test
# Run specific test file
flutter test test/watch_it_test.dart
# Run tests with coverage
flutter test --coverage# Analyze code
flutter analyze
# Format code (REQUIRED before commits)
dart format .
# Dry run publish check
flutter pub publish --dry-runcd example
flutter run
# Run on specific device
flutter run -d chrome# Get dependencies
flutter pub get
# Upgrade dependencies (check compatibility first)
flutter pub upgradeCritical: watch_it uses a global variable _activeWatchItState (in elements.dart) that holds the current widget's watch state during build. This is the "magic" that allows watch functions to work without explicit parameters.
How it works:
- When a widget with
WatchItMixinorWatchingWidgetbuilds, itsElementsets_activeWatchItStateto its local_WatchItStateinstance - All
watch*()function calls access_activeWatchItStateto register watches - After build completes,
_activeWatchItStateis reset to null - Similar pattern to
flutter_hooksand React Hooks
Code location: lib/src/elements.dart:3-32 - _WatchItElement mixin
CRITICAL RULE: All watch*() and registerHandler*() calls MUST:
- Be called inside
build()method - Be called in the SAME ORDER on every build
- Not be conditional (no
ifstatements wrapping watch calls) - Not be inside builders/callbacks
Why: Each watch call corresponds to a position in _watchList (see watch_it_state.dart:78). On rebuild, the counter resets and each watch call retrieves its previous _WatchEntry by index. Changing order breaks this mapping.
Implementation: lib/src/watch_it_state.dart:138-175
resetCurrentWatch()- Resets counter to 0 at start of build_getWatch()- Retrieves watch entry by current index, increments counter_appendWatch()- Adds new watch entry when first encountered
Three ways to use watch_it:
- WatchingWidget (extends StatelessWidget) -
lib/src/widgets.dart - WatchingStatefulWidget (extends StatefulWidget) -
lib/src/widgets.dart - Mixins:
WatchItMixinorWatchItStatefulWidgetMixin-lib/src/mixins.dart
All create custom Element subclasses (_StatelessWatchItElement or _StatefulWatchItElement) that:
- Initialize
_WatchItStateon mount - Set/unset
_activeWatchItStatearound build - Dispose watch entries on unmount
Hierarchy:
Listenable (base)
├─ ChangeNotifier
└─ ValueListenable<T>
└─ ValueNotifier<T>
Watch function mapping:
watch()- AnyListenable(ChangeNotifier, ValueNotifier, etc.)watchIt()-Listenablefrom get_itwatchValue()-ValueListenableproperty from get_it objectwatchPropertyValue()- Property ofListenable, rebuilds only when property value changeswatchStream()-Stream<T>, returnsAsyncSnapshot<T>watchFuture()-Future<T>, returnsAsyncSnapshot<T>
Implementation: All in lib/src/watch_it.dart
Handlers execute side effects (show dialogs, navigation, etc.) instead of rebuilding:
registerHandler()- ForValueListenablechangesregisterChangeNotifierHandler()- ForChangeNotifierchangesregisterStreamHandler()- ForStreameventsregisterFutureHandler()- ForFuturecompletion
Key difference: Handlers receive a cancel() function to unsubscribe from inside the handler.
createOnce()- Create objects on first build, auto-dispose on widget destroycreateOnceAsync()- Async version, returnsAsyncSnapshot<T>callOnce()- Execute function once on first buildonDispose()- Register dispose callbackpushScope()- Push get_it scope tied to widget lifecycle
Use case: Creating TextEditingController, AnimationController, etc. in stateless widgets
Two-level tracing system:
- Widget-level: Call
enableTracing()at start of build - Subtree-level: Wrap with
WatchItSubTreeTraceControlwidget
Performance consideration: Subtree tracing only active if enableSubTreeTracing = true globally (checked in _checkSubTreeTracing())
Custom logging: Override watchItLogFunction to integrate with analytics/monitoring
Events tracked: WatchItEvent enum in watch_it_tracing.dart - rebuild, handler, createOnce, scopePush, etc.
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
// Always at top of build, same order every time
final user = watchIt<UserModel>();
final count = watchValue((CounterModel m) => m.count);
final name = watchPropertyValue((UserModel m) => m.name);
return Text('$name: $count');
}
}class MyWidget extends StatelessWidget with WatchItMixin {
@override
Widget build(BuildContext context) {
registerHandler(
select: (ErrorModel m) => m.lastError,
handler: (context, error, cancel) {
if (error != null) {
showDialog(context: context, builder: (_) => ErrorDialog(error));
cancel(); // Stop listening after first error
}
},
);
return Container();
}
}class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final ready = allReady(
onReady: (context) => Navigator.pushReplacement(...),
timeout: Duration(seconds: 5),
);
if (!ready) return CircularProgressIndicator();
return MainContent();
}
}- Setup: Always call
GetIt.I.reset()intearDown - Pump widgets: Use
pumpWidget()to trigger builds - Verify rebuilds: Track state changes, verify widget updates
- Test ordering: Verify watch calls maintain order across rebuilds
Test file: test/watch_it_test.dart
testWidgets('watch rebuilds on notify', (tester) async {
final model = TestModel();
GetIt.I.registerSingleton(model);
await tester.pumpWidget(
MaterialApp(home: TestWidget()),
);
expect(find.text('0'), findsOneWidget);
model.increment(); // Triggers notifyListeners()
await tester.pump();
expect(find.text('1'), findsOneWidget);
});- Access global state: Use
_activeWatchItState(assert it's not null) - Delegate to _WatchItState: Don't implement watch logic in global functions
- Maintain order invariant: Document that function must be called in same order
- Support both get_it and local: Provide
targetparameter for local observables when possible
- Index management: Carefully handle
currentWatchIndexin_getWatch()andresetCurrentWatch() - Dispose properly: Every
_WatchEntrymust clean up listeners/subscriptions in its dispose function - Null safety: Check
_element != nullbefore calling handlers (can be called after dispose) - Tracing: Add appropriate trace points for new functions
- Follow lifecycle pattern: Use
_getWatch()/_appendWatch()pattern - Provide eventType: Add new
WatchItEventenum value if needed - Document ordering requirement: Make clear in docs/asserts if order matters
- get_it: ^8.0.0 - Service locator (foundation)
- functional_listener: ^4.0.0 - Advanced listenable utilities
- flutter: SDK
Compatibility: Flutter >=3.0.0, Dart >=2.19.6 <4.0.0
- Widget must extend
WatchingWidget/WatchingStatefulWidgetOR useWatchItMixin - Watch calls must be directly in build method, not in callbacks
- Can't call
watch()orwatchIt()twice on same object - Use
watchPropertyValue()for multiple properties of same object - Handlers are exempt (can register multiple handlers on same object)
watchFuture/watchStreamselector returns NEW Future/Stream each build- Solution: Return the SAME Future/Stream instance (store in object)
- Conditional watch calls change order between builds
- Solution: Move watch calls to top of build, use conditional rendering AFTER
lib/
├── watch_it.dart # Main export, global di/sl instances
├── src/
├── elements.dart # Element mixins, global _activeWatchItState
├── mixins.dart # WatchItMixin, WatchItStatefulWidgetMixin
├── watch_it_state.dart # Core _WatchItState class, _WatchEntry
├── watch_it.dart # All watch*() and register*() global functions
├── watch_it_tracing.dart # Tracing infrastructure, WatchItEvent enum
└── widgets.dart # WatchingWidget, WatchingStatefulWidget
- Update
CHANGELOG.mdwith version and changes - Update version in
pubspec.yaml - Run
dart format . - Run
flutter analyze(must pass) - Run
flutter test(must pass) - Run
flutter pub publish --dry-run - Commit changes
- Create git tag:
git tag vX.Y.Z - Push with tags:
git push --tags - Run
flutter pub publish
- Documentation site: https://flutter-it.dev
- GitHub: https://github.com/escamoteur/watch_it
- Discord: https://discord.gg/ZHYHYCM38h
- get_it package: https://pub.dev/packages/get_it
- don't stop to announce something if you haven't finished