listenable_collections 3.0.0
listenable_collections: ^3.0.0 copied to clipboard
A collection of Dart collections that behave like ValueNotifier if their data changes.
📢 Important Notice
This is the last release under the
listenable_collectionsname. In future versions, these features will be integrated into thelisten_itpackage.For more information, visit flutter-it.dev
listenable_collections #
Reactive Dart collections that integrate with Flutter's ValueListenableBuilder and watch_it
Extend your Lists, Maps, and Sets with automatic change notifications - perfect for reactive UI updates in Flutter without complex state management solutions.
✨ Features #
- 🔔 Automatic Notifications - Collections notify listeners on every mutation
- 📦 Three Collection Types -
ListNotifier,MapNotifier,SetNotifier - 🎯 Three Notification Modes - Fine-grained control over when notifications fire
- ⚡ Transaction Support - Batch multiple operations into a single notification
- 🔒 Immutability - Value getters return unmodifiable views
- 🎨 Flutter Integration - Works with
ValueListenableBuilderorwatch_it
📦 Installation #
dependencies:
listenable_collections: ^1.0.0
🚀 Quick Start #
Creating Reactive Collections #
Simply wrap your collection type with a notifier:
// Instead of:
final items = <String>[];
// Use:
final items = ListNotifier<String>();
// With initial data:
final items = ListNotifier<String>(data: ['item1', 'item2']);
All standard collection methods work as expected - the difference is they now notify listeners!
Integration with Flutter #
Option 1: Using ValueListenableBuilder (built-in)
class TodoListWidget extends StatelessWidget {
final todos = ListNotifier<String>();
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<List<String>>(
valueListenable: todos,
builder: (context, items, _) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => Text(items[index]),
);
},
);
}
}
Option 2: Using watch_it (cleaner syntax)
// Add watch_it dependency first
dependencies:
watch_it: ^1.0.0
class TodoListWidget extends WatchingWidget {
final todos = ListNotifier<String>();
@override
Widget build(BuildContext context) {
final items = watch(todos).value;
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => Text(items[index]),
);
}
}
🎯 Notification Modes #
Choose when your UI should update:
// Normal mode - only notify on actual changes
final cart = SetNotifier<String>(
notificationMode: CustomNotifierMode.normal,
);
cart.add('item1'); // ✅ Notifies (new item)
cart.add('item1'); // ❌ No notification (already exists)
// Always mode - notify on every operation (default)
final cart = SetNotifier<String>(
notificationMode: CustomNotifierMode.always,
);
cart.add('item1'); // ✅ Notifies
cart.add('item1'); // ✅ Notifies (even though already exists)
// Manual mode - you control when to notify
final cart = SetNotifier<String>(
notificationMode: CustomNotifierMode.manual,
);
cart.add('item1');
cart.add('item2');
cart.notifyListeners(); // ✅ Single notification for both adds
Why the default is always?
If users haven't overridden == operator, they expect UI updates even when setting the "same" value.
📊 Choosing the Right Collection #
| Collection | Use When | Example Use Case |
|---|---|---|
ListNotifier<T> |
Order matters, duplicates allowed | Todo list, chat messages, search history |
MapNotifier<K,V> |
Need key-value lookups | User preferences, caches, form data |
SetNotifier<T> |
Unique items only, fast membership tests | Selected item IDs, active filters, tags |
⚡ Batch Updates with Transactions #
Avoid unnecessary rebuilds by batching operations:
final products = ListNotifier<Product>();
// Without transaction: 3 UI updates
products.add(product1);
products.add(product2);
products.add(product3);
// With transaction: 1 UI update
products.startTransAction();
products.add(product1);
products.add(product2);
products.add(product3);
products.endTransAction();
🔍 Real-World Examples #
Shopping Cart Manager #
class ShoppingCart {
final items = MapNotifier<String, CartItem>();
void addItem(Product product) {
items[product.id] = CartItem(product: product, quantity: 1);
}
void updateQuantity(String productId, int quantity) {
if (quantity <= 0) {
items.remove(productId);
} else {
items[productId] = items[productId]!.copyWith(quantity: quantity);
}
}
double get total => items.values
.fold(0.0, (sum, item) => sum + item.price * item.quantity);
}
// In your widget with watch_it:
class CartSummary extends WatchingWidget {
final ShoppingCart cart;
@override
Widget build(BuildContext context) {
final itemCount = watch(cart.items).value.length;
return Text('$itemCount items in cart');
}
}
Multi-Select with Sets #
class FilterManager {
final selectedFilters = SetNotifier<String>(
notificationMode: CustomNotifierMode.normal,
);
void toggleFilter(String filter) {
if (selectedFilters.contains(filter)) {
selectedFilters.remove(filter);
} else {
selectedFilters.add(filter);
}
}
bool isSelected(String filter) => selectedFilters.contains(filter);
}
// In your widget:
ValueListenableBuilder<Set<String>>(
valueListenable: filterManager.selectedFilters,
builder: (context, selected, _) {
return Wrap(
children: availableFilters.map((filter) {
return FilterChip(
label: Text(filter),
selected: selected.contains(filter),
onSelected: (_) => filterManager.toggleFilter(filter),
);
}).toList(),
);
},
)
Message Queue with Transactions #
class MessageQueue {
final messages = ListNotifier<Message>(
notificationMode: CustomNotifierMode.normal,
);
Future<void> loadBatch(List<Message> newMessages) async {
messages.startTransAction();
messages.clear();
messages.addAll(newMessages);
messages.endTransAction(); // Single rebuild
}
}
🎨 Collection-Specific Features #
ListNotifier #
- Index-based access:
list[0] = newValue - Custom equality: Only notify when values truly differ
- Reordering: Built-in
swap(index1, index2)method - Smart operations:
fillRange,replaceRangecompare values in normal mode
MapNotifier #
- Smart putIfAbsent: In
alwaysmode, notifies even when key exists - Null-safe:
remove()only notifies if key existed - Custom equality: Compare values, not just references
SetNotifier #
- Duplicate detection:
add()returns bool (was it actually added?) - No custom equality: Uses Set's built-in
==andhashCode(less confusing) - Set operations:
union(),intersection(),difference()are read-only (don't notify)
🆚 When to Use This vs Alternatives #
Use listenable_collections when:
- ✅ You have simple local state (a list, map, or set)
- ✅ You want minimal boilerplate
- ✅ You're already comfortable with
ValueNotifier - ✅ You need fine-grained control (notification modes, transactions)
Consider alternatives when:
- ❌ You need global state management → Use
Provider,Riverpod, orget_itwithwatch_it - ❌ You need computed/derived state → Use
ProviderorRiverpod - ❌ You need state persistence → Combine with
shared_preferencesor state management solution - ❌ You have complex state logic → Use BLoC, Riverpod, or Redux patterns
🔧 Advanced Features #
Custom Equality (List and Map only) #
// Only notify when user ID changes (ignore other fields)
final users = ListNotifier<User>(
customEquality: (a, b) => a.id == b.id,
notificationMode: CustomNotifierMode.normal,
);
users[0] = updatedUser; // No notification if ID is the same
Note: SetNotifier intentionally doesn't support custom equality to avoid confusion with Set's inherent == and hashCode behavior.
Immutable Snapshots #
final list = ListNotifier<int>(data: [1, 2, 3]);
// Get immutable snapshot
final snapshot = list.value; // UnmodifiableListView<int>
// This throws UnsupportedError:
snapshot.add(4); // ❌ Can't modify the snapshot
// To modify, use the notifier directly:
list.add(4); // ✅ This works and notifies listeners
Integration with watch_it #
For cleaner syntax and better ergonomics:
class MyWidget extends WatchingWidget {
final items = ListNotifier<String>();
@override
Widget build(BuildContext context) {
// Clean, readable syntax
final itemList = watch(items).value;
return Column(
children: itemList.map((item) => Text(item)).toList(),
);
}
}
📚 API Documentation #
Detailed API documentation is available on pub.dev:
🤝 Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
📄 License #
MIT License - see the LICENSE file for details.