Skip to content

Commit 83dd967

Browse files
committed
Release v2.1.0 - Better error messages and documentation
- Improved error messages for watch ordering violations with clear guidance - Added comprehensive watch ordering documentation to README - Increased test coverage to 93.3%
1 parent 102d7c8 commit 83dd967

File tree

5 files changed

+473
-42
lines changed

5 files changed

+473
-42
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 2.1.0
2+
3+
### Improvements
4+
* **Better Error Messages**: Watch ordering violations now show a helpful error message instead of cryptic type errors. When conditional watch calls are placed incorrectly, you'll see clear guidance on how to fix it with BAD/GOOD examples.
5+
* **Enhanced Documentation**: Added "Watch Ordering and Conditional Watches" section to README with visual examples explaining why conditional watches must be at the end of build methods.
6+
* **Improved Test Coverage**: Test coverage increased to 93.3%.
7+
18
## 2.0.1
29

310
### Maintenance

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,93 @@ There are some important rules to follow in order to avoid bugs with the `watch`
239239

240240
If you want to know more about the reasons for this rule check out [Lifting the magic curtain](#lifting-the-magic-curtain)
241241

242+
## Watch Ordering and Conditional Watches
243+
244+
The most common mistake when using WatchIt is placing conditional `watch` calls in the middle of your build method. **All `watch` calls must happen in the same order on every build.**
245+
246+
### The Problem
247+
248+
When you have conditional `watch` calls that come before other `watch` calls, WatchIt can lose track of which watch corresponds to which data. This happens because watches are stored in a list indexed by their call order.
249+
250+
**❌ BAD - Conditional in the middle:**
251+
252+
```dart
253+
class MyWidget extends StatelessWidget with WatchItMixin {
254+
@override
255+
Widget build(BuildContext context) {
256+
final listing = watchIt<Listing>();
257+
258+
// Conditional watch BEFORE other watches
259+
if (listing.hasDetails) {
260+
watch(listing.details!); // ❌ This is at index 1 SOMETIMES
261+
}
262+
263+
// These watches will have different indices depending on the conditional
264+
final count = watchValue((Model m) => m.count); // Index 1 or 2?
265+
final name = watchValue((Model m) => m.name); // Index 2 or 3?
266+
267+
return Text('$count - $name');
268+
}
269+
}
270+
```
271+
272+
When `listing.hasDetails` changes from `true` to `false`, the indices shift and WatchIt tries to retrieve the wrong watch entries. You'll see a helpful error message:
273+
274+
```
275+
Watch ordering violation detected!
276+
277+
You have conditional watch calls (inside if/switch statements) that are
278+
causing watch_it to retrieve the wrong objects on rebuild.
279+
280+
Fix: Move ALL conditional watch calls to the END of your build method.
281+
Only the LAST watch call can be conditional.
282+
283+
Example - BAD:
284+
watch(model);
285+
if (condition) { watch(optional); } // ← Problem!
286+
watchValue((M m) => m.property); // ← Gets wrong type
287+
288+
Example - GOOD:
289+
watch(model);
290+
watchValue((M m) => m.property);
291+
if (condition) { watch(optional); } // ← At the end: OK
292+
293+
Widget: MyWidget
294+
```
295+
296+
### The Solution
297+
298+
**✅ GOOD - Conditional at the end:**
299+
300+
```dart
301+
class MyWidget extends StatelessWidget with WatchItMixin {
302+
@override
303+
Widget build(BuildContext context) {
304+
final listing = watchIt<Listing>();
305+
306+
// All non-conditional watches FIRST
307+
final count = watchValue((Model m) => m.count); // Always index 1
308+
final name = watchValue((Model m) => m.name); // Always index 2
309+
310+
// Conditional watch at the END
311+
if (listing.hasDetails) {
312+
watch(listing.details!); // ✅ Only the last watch can be conditional
313+
}
314+
315+
return Text('$count - $name');
316+
}
317+
}
318+
```
319+
320+
When the conditional watch is at the end, adding or removing it doesn't affect the indices of previous watches.
321+
322+
### Key Takeaways
323+
324+
-**All non-conditional watches first**: Put regular `watch` calls at the top of your build method
325+
-**Conditionals at the end**: Only the LAST watch call can be inside an `if`/`switch` statement
326+
-**Same order every build**: Each `watch` call must happen at the same position on every build
327+
-**No conditionals in the middle**: Don't put `watch` calls inside conditionals if other watches follow them
328+
242329
# The watch functions in detail:
243330

244331
## Watching `Listenable / ChangeNotifier`

lib/src/watch_it_state.dart

Lines changed: 81 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,60 @@ class _WatchItState {
146146

147147
/// if _getWatch returns null it means this is either the very first or the las watch
148148
/// in this list.
149-
_WatchEntry? _getWatch() {
149+
/// Performs type checking to catch watch ordering violations early with helpful error messages.
150+
_WatchEntry<T, V>? _getWatch<T, V>() {
150151
if (currentWatchIndex != null) {
151152
assert(_watchList.length > currentWatchIndex!);
152153
final result = _watchList[currentWatchIndex!];
153-
currentWatchIndex = currentWatchIndex! + 1;
154-
if (currentWatchIndex! == _watchList.length) {
155-
currentWatchIndex = null;
154+
155+
// Type check with helpful error message for watch ordering violations
156+
try {
157+
final typedResult = result as _WatchEntry<T, V>;
158+
currentWatchIndex = currentWatchIndex! + 1;
159+
if (currentWatchIndex! == _watchList.length) {
160+
currentWatchIndex = null;
161+
}
162+
return typedResult;
163+
} on TypeError catch (_) {
164+
// Build error message with source location if available
165+
final buffer = StringBuffer('Watch ordering violation detected!\n\n');
166+
167+
buffer.writeln(
168+
'You have conditional watch calls (inside if/switch statements) that are');
169+
buffer.writeln(
170+
'causing watch_it to retrieve the wrong objects on rebuild.');
171+
172+
// Add source location if tracing was enabled
173+
if (result.sourceLocationOfWatch != null) {
174+
buffer.writeln('\nConflicting watch entry was created at:');
175+
buffer.writeln(result.sourceLocationOfWatch);
176+
buffer.writeln('\nLook for a watch statement that returns type: $V');
177+
}
178+
179+
buffer.writeln(
180+
'\nFix: Move ALL conditional watch calls to the END of your build method.');
181+
buffer.writeln('Only the LAST watch call can be conditional.');
182+
buffer.writeln('\nExample - BAD:');
183+
buffer.writeln(' watch(model);');
184+
buffer.writeln(' if (condition) { watch(optional); } // ← Problem!');
185+
buffer.writeln(
186+
' watchValue((M m) => m.property); // ← Gets wrong type');
187+
buffer.writeln('\nExample - GOOD:');
188+
buffer.writeln(' watch(model);');
189+
buffer.writeln(' watchValue((M m) => m.property);');
190+
buffer.writeln(
191+
' if (condition) { watch(optional); } // ← At the end: OK');
192+
193+
// Suggest enabling tracing if source location wasn't available
194+
if (result.sourceLocationOfWatch == null) {
195+
buffer.writeln(
196+
'\nTip: Call enableTracing() in your build method to see exact source locations.');
197+
}
198+
199+
buffer.writeln('\nWidget: ${_element?.widget.runtimeType}');
200+
201+
throw StateError(buffer.toString());
156202
}
157-
return result;
158203
}
159204
return null;
160205
}
@@ -184,7 +229,7 @@ class _WatchItState {
184229
handler,
185230
bool executeImmediately = false,
186231
}) {
187-
var watch = _getWatch() as _WatchEntry<Listenable, R>?;
232+
var watch = _getWatch<Listenable, R>();
188233

189234
Listenable actualTarget;
190235

@@ -300,7 +345,7 @@ class _WatchItState {
300345
required R Function(T) only,
301346
Object? parentObject,
302347
}) {
303-
var watch = _getWatch() as _WatchEntry<Listenable, R>?;
348+
var watch = _getWatch<Listenable, R>();
304349

305350
if (watch != null) {
306351
if (listenable != watch.observedObject) {
@@ -355,7 +400,7 @@ class _WatchItState {
355400
Stream<R> Function(T)? selector,
356401
bool allowStreamChange = true,
357402
}) {
358-
var watch = _getWatch() as _WatchEntry<Stream<R>, AsyncSnapshot<R?>>?;
403+
var watch = _getWatch<Stream<R>, AsyncSnapshot<R?>>();
359404
Stream<R> actualStream;
360405

361406
if (watch != null) {
@@ -517,6 +562,28 @@ class _WatchItState {
517562
allowStreamChange: allowStreamChange);
518563
}
519564

565+
/// Helper to call handler if conditions are met
566+
/// Returns true if handler was called
567+
bool _callFutureHandlerIfNeeded<R>(
568+
void Function(BuildContext context, AsyncSnapshot<R?> snapshot,
569+
void Function() cancel)?
570+
handler,
571+
_WatchEntry<Future<R>, AsyncSnapshot<R>> watch,
572+
bool callHandlerOnlyOnce,
573+
) {
574+
if (handler != null &&
575+
_element != null &&
576+
(!watch.handlerWasCalled || !callHandlerOnlyOnce)) {
577+
if (_logHandlers) {
578+
watch._logWatchItEvent();
579+
}
580+
handler(_element!, watch.lastValue!, watch.dispose);
581+
watch.handlerWasCalled = true;
582+
return true;
583+
}
584+
return false;
585+
}
586+
520587
/// this function is used to implement several others
521588
/// therefore not all parameters will be always used
522589
/// [initialValueProvider] can return an initial value that is returned
@@ -546,7 +613,7 @@ class _WatchItState {
546613
bool isCreateOnceAsync = false,
547614
Future<R> Function(T)? selector,
548615
bool allowFutureChange = true}) {
549-
var watch = _getWatch() as _WatchEntry<Future<R>, AsyncSnapshot<R>>?;
616+
var watch = _getWatch<Future<R>, AsyncSnapshot<R>>();
550617

551618
Future<R>? future;
552619

@@ -557,32 +624,14 @@ class _WatchItState {
557624
future = watch.observedObject;
558625

559626
/// still the same Future so we can directly return last value
560-
if (handler != null &&
561-
_element != null &&
562-
(!watch.handlerWasCalled || !callHandlerOnlyOnce)) {
563-
if (_logHandlers) {
564-
watch._logWatchItEvent();
565-
}
566-
handler(_element!, watch.lastValue!, watch.dispose);
567-
watch.handlerWasCalled = true;
568-
}
569-
627+
_callFutureHandlerIfNeeded(handler, watch, callHandlerOnlyOnce);
570628
return watch.lastValue!;
571629
} else if (futureProvider != null) {
572630
/// still the same Future so we can directly return last value
573631
/// in case that we got a futureProvider we always keep the first
574632
/// returned Future
575633
/// and call the Handler again as the state hasn't changed
576-
if (handler != null &&
577-
_element != null &&
578-
(!watch.handlerWasCalled || !callHandlerOnlyOnce)) {
579-
if (_logHandlers) {
580-
watch._logWatchItEvent();
581-
}
582-
handler(_element!, watch.lastValue!, watch.dispose);
583-
watch.handlerWasCalled = true;
584-
}
585-
634+
_callFutureHandlerIfNeeded(handler, watch, callHandlerOnlyOnce);
586635
return watch.lastValue!;
587636
} else {
588637
// Get the future from selector or parentOrFuture
@@ -599,16 +648,7 @@ class _WatchItState {
599648
if (future == watch.observedObject) {
600649
/// still the same Future so we can directly return last value
601650
/// and call the Handler again as the state hasn't changed
602-
if (handler != null &&
603-
_element != null &&
604-
(!watch.handlerWasCalled || !callHandlerOnlyOnce)) {
605-
if (_logHandlers) {
606-
watch._logWatchItEvent();
607-
}
608-
handler(_element!, watch.lastValue!, watch.dispose);
609-
watch.handlerWasCalled = true;
610-
}
611-
651+
_callFutureHandlerIfNeeded(handler, watch, callHandlerOnlyOnce);
612652
return watch.lastValue!;
613653
} else {
614654
/// Future identity changed
@@ -735,7 +775,7 @@ class _WatchItState {
735775
}
736776

737777
T createOnce<T>(T Function() factoryFunc, {void Function(T value)? dispose}) {
738-
var watch = _getWatch() as _WatchEntry<void, T>?;
778+
var watch = _getWatch<void, T>();
739779

740780
if (watch == null) {
741781
final value = factoryFunc();
@@ -963,7 +1003,7 @@ class _WatchItState {
9631003

9641004
void callAfterEveryBuild(
9651005
void Function(BuildContext context, void Function() cancel) callback) {
966-
var watch = _getWatch() as _WatchEntry<void, bool>?;
1006+
var watch = _getWatch<void, bool>();
9671007

9681008
if (watch == null) {
9691009
// First time - create the watch entry with a cancelled flag

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: watch_it
22
description: The simple state management powered by get_it. It allows to observe changes of objects inside the get_it service locator and rebuild the UI accordingly.
3-
version: 2.0.1
3+
version: 2.1.0
44
homepage: https://github.com/escamoteur/watch_it
55
funding:
66
- https://github.com/sponsors/escamoteur/

0 commit comments

Comments
 (0)