listen-it-expert
SKILL.md
listen_it Expert - ValueListenable Operators & Reactive Collections
What: Extension methods on ValueListenable/Listenable for transformations, filtering, debouncing. Plus reactive collections. Pure Dart.
CRITICAL RULES
- Operators return NEW ValueListenable objects - MUST capture the result
listen()signature differs: onListenablegets(subscription), onValueListenablegets(value, subscription)mergeWith()is an INSTANCE method on ValueListenable, NOT a static method- There is NO
throttle()operator - NEVER create operator chains inline in
build()- memory leak! Use class fields or watch_it - All operators support
{bool lazy = false}for deferred initialization
listen() - Subscribing to Changes
// On ValueListenable<T> - receives value AND subscription
final sub = counter.listen((int value, ListenableSubscription subscription) {
print('New value: $value');
if (value > 100) subscription.cancel(); // Self-cancel
});
sub.cancel(); // Or cancel externally
// On plain Listenable - receives ONLY subscription (no value)
final sub = myChangeNotifier.listen((ListenableSubscription subscription) {
print('Something changed');
});
Transformation Operators
All return new ValueListenable and support {bool lazy = false}:
final counter = ValueNotifier<int>(0);
// map - Transform values
final doubled = counter.map((x) => x * 2);
// select - Like map but only notifies when result CHANGES (equality check)
final user = ValueNotifier<User>(User('Alice', 30));
final name = user.select((u) => u.name); // Only fires when name actually changes
// where - Filter values (only propagate when test passes)
final positives = counter.where((x) => x > 0);
final positives = counter.where((x) => x > 0, fallbackValue: 0); // Default when filtered
// debounce - Delay notifications
final debouncedSearch = searchField.debounce(Duration(milliseconds: 300));
// async - Defer to next frame (prevents setState-during-build)
final deferred = counter.async();
Combining Operators
// combineLatest - Merge 2 ValueListenables
final firstName = ValueNotifier<String>('Alice');
final lastName = ValueNotifier<String>('Smith');
final fullName = firstName.combineLatest(
lastName,
(first, last) => '$first $last',
);
// combineLatest3/4/5/6 - Merge up to 6 sources
final combined = source1.combineLatest3(
source2, source3,
(v1, v2, v3) => '$v1-$v2-$v3',
);
// mergeWith - Merge multiple sources of SAME type (instance method)
final merged = source1.mergeWith([source2, source3]);
// Emits whenever any source changes, value is from the source that changed
Chaining Operators
final processed = searchInput
.where((text) => text.isNotEmpty)
.map((text) => text.trim().toLowerCase())
.debounce(Duration(milliseconds: 300));
processed.listen((query, subscription) {
performSearch(query);
});
Reactive Collections
Auto-notify listeners on mutations:
// ListNotifier
final items = ListNotifier<String>(data: ['a', 'b', 'c']);
items.add('d'); // Notifies
items.remove('a'); // Notifies
items[0] = 'z'; // Notifies
items.value; // UnmodifiableListView (read-only access)
// MapNotifier
final settings = MapNotifier<String, dynamic>(data: {'theme': 'dark'});
settings['theme'] = 'light'; // Notifies
settings.remove('theme'); // Notifies
// SetNotifier
final tags = SetNotifier<String>(data: {'flutter', 'dart'});
tags.add('mobile'); // Notifies
tags.remove('dart'); // Notifies
Notification modes:
// CustomNotifierMode.always - notify on every operation (default for collections)
// CustomNotifierMode.normal - notify only when value changes (== check)
// CustomNotifierMode.manual - no auto-notification, call notifyListeners() yourself
final items = ListNotifier<String>(
notificationMode: CustomNotifierMode.always,
);
Transactions (batch operations, single notification):
items.startTransAction();
items.add('a');
items.add('b');
items.add('c');
items.endTransAction(); // Single notification for all 3 adds
Anti-Patterns
// ❌ Not capturing operator result
counter.map((x) => x * 2); // Lost! Nobody holds a reference
// ✅ Capture it
final doubled = counter.map((x) => x * 2);
// ❌ Creating chains inline in build() - MEMORY LEAK
Widget build(context) {
final doubled = counter.map((x) => x * 2); // New chain every build!
...
}
// ✅ Use class field or watch_it (caches selector automatically)
late final doubled = counter.map((x) => x * 2); // Created once
// ❌ Using addListener instead of listen
notifier.addListener(() { print(notifier.value); });
// ✅ Use listen() from listen_it
notifier.listen((value, sub) { print(value); });
// ❌ ValueNotifier.merge (doesn't exist!)
ValueNotifier.merge([a, b], combiner); // NOT A REAL METHOD
// ✅ Use mergeWith (instance method) or combineLatest
final merged = a.mergeWith([b]);
final combined = a.combineLatest(b, (va, vb) => va + vb);
Production Patterns
Debounced auto-save:
_dataSubscription = _data
.debounce(const Duration(seconds: 1))
.listen((_, __) {
_data.saveDraft();
});
Filtered draft list:
final ListNotifier<CommonComposerData> _drafts = ListNotifier(
notificationMode: CustomNotifierMode.manual,
);
late ValueListenable<List<CommonComposerData>> savedDrafts =
(_drafts as ValueListenable<List<CommonComposerData>>)
.map((list) => list.where((e) => e.intentionallySaved).toList());
Error listening on commands:
updatePostCommand.errors.listen((error, _) {
final composerData = error!.paramData;
composerData?.saveDraft(withIntention: true);
});
CustomValueNotifier
For advanced notification control:
final notifier = CustomValueNotifier<int>(
0,
mode: CustomNotifierMode.normal, // Only notify on actual changes
asyncNotification: false, // true = defer to next frame
);
Weekly Installs
2
Repository
flutter-it/flutter_itGitHub Stars
13
First Seen
2 days ago
Security Audits
Installed on
amp2
cline2
opencode2
cursor2
kimi-cli2
codex2