collection_notifiers
Reactive collections for Flutter — Lists, Sets, Maps, and Queues that automatically rebuild your UI when they change.
✨ Why collection_notifiers?
| Without this package | With collection_notifiers |
|---|---|
| Create copies on every change | Mutate in place |
| Always triggers rebuilds | Only rebuilds when actually changed |
| Verbose state management code | Clean, simple API |
| Manual equality checks | Automatic optimization |
// ❌ Traditional approach - creates new objects, always rebuilds
ref.read(provider.notifier).update((state) => {...state, newItem});
// ✅ With collection_notifiers - zero copies, smart rebuilds
ref.read(provider).add(newItem);
📦 Installation
Add to your pubspec.yaml:
dependencies:
collection_notifiers: ^2.0.0
Then run:
flutter pub get
🚀 Quick Start
1. Create a reactive collection
import 'package:collection_notifiers/collection_notifiers.dart';
// Just like regular collections, but reactive!
final todos = ListNotifier<String>(['Buy milk', 'Walk dog']);
final selectedIds = SetNotifier<int>();
final settings = MapNotifier<String, bool>({'darkMode': false});
2. Use with ValueListenableBuilder
ValueListenableBuilder<List<String>>(
valueListenable: todos,
builder: (context, items, child) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => Text(items[index]),
);
},
)
3. Mutate and watch UI update automatically
todos.add('Call mom'); // ✅ UI rebuilds
todos[0] = 'Buy eggs'; // ✅ UI rebuilds
todos[0] = 'Buy eggs'; // ⏭️ No rebuild (same value!)
📚 Available Notifiers
| Type | Class | Best For |
|---|---|---|
| List | ListNotifier<E> |
Ordered items, indices matter |
| Set | SetNotifier<E> |
Unique items, selections |
| Map | MapNotifier<K,V> |
Key-value data, settings |
| Queue | QueueNotifier<E> |
FIFO/LIFO operations |
🎯 Smart Notifications
The magic is in the optimization — methods only notify listeners when something actually changes:
final tags = SetNotifier<String>({'flutter', 'dart'});
tags.add('rust'); // 🔔 Notifies — new element added
tags.add('rust'); // 🔕 Silent — already exists
tags.remove('rust'); // 🔔 Notifies — element removed
tags.remove('rust'); // 🔕 Silent — wasn't there
tags.clear(); // 🔔 Notifies — set emptied
tags.clear(); // 🔕 Silent — already empty
Same for Maps:
final config = MapNotifier<String, int>({'volume': 50});
config['volume'] = 75; // 🔔 Notifies — value changed
config['volume'] = 75; // 🔕 Silent — same value
config['bass'] = 30; // 🔔 Notifies — new key added
💡 Common Patterns
Selection UI with SetNotifier
Perfect for checkboxes, chips, and multi-select:
final selected = SetNotifier<int>();
// In your widget
CheckboxListTile(
value: selected.contains(itemId),
onChanged: (_) => selected.invert(itemId), // Toggle with one call!
title: Text('Item $itemId'),
)
The invert() method toggles presence:
- If item exists → removes it, returns
false - If item missing → adds it, returns
true
Settings with MapNotifier
final settings = MapNotifier<String, dynamic>({
'darkMode': false,
'fontSize': 14,
'notifications': true,
});
// Toggle dark mode
settings['darkMode'] = !settings['darkMode']!;
// Only rebuilds if value actually changes
settings['fontSize'] = 14; // No rebuild if already 14
Todo List with ListNotifier
final todos = ListNotifier<Todo>();
// Add
todos.add(Todo(title: 'Learn Flutter'));
// Remove
todos.removeWhere((t) => t.completed);
// Reorder
final item = todos.removeAt(oldIndex);
todos.insert(newIndex, item);
// Sort
todos.sort((a, b) => a.priority.compareTo(b.priority));
🔌 State Management Integration
With Riverpod
final todosProvider = ChangeNotifierProvider((ref) {
return ListNotifier<String>(['Initial todo']);
});
// In widget
class TodoList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todosProvider);
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, i) => ListTile(
title: Text(todos[i]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => ref.read(todosProvider).removeAt(i),
),
),
);
}
}
With Provider
ChangeNotifierProvider(
create: (_) => SetNotifier<int>(),
child: MyApp(),
)
// In widget
final selected = context.watch<SetNotifier<int>>();
context.read<SetNotifier<int>>().add(itemId);
Vanilla Flutter (no packages)
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final _items = ListNotifier<String>();
@override
void dispose() {
_items.dispose(); // Don't forget to dispose!
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<List<String>>(
valueListenable: _items,
builder: (context, items, _) => /* your UI */,
);
}
}
⚠️ Important Notes
Element Equality
Smart notifications rely on == comparison. For custom objects:
// ❌ Won't work - default object equality
class User {
final String name;
User(this.name);
}
// ✅ Works - proper equality
class User {
final String name;
User(this.name);
@override
bool operator ==(Object other) => other is User && other.name == name;
@override
int get hashCode => name.hashCode;
}
Pro tip: Use freezed or equatable for automatic equality.
Always Dispose
When using in StatefulWidgets, always dispose:
@override
void dispose() {
myNotifier.dispose();
super.dispose();
}
Some Methods Always Notify
sort() and shuffle() always notify because checking if order changed would be expensive.
📖 Migration from 2.x
Breaking change: SetNotifier.invert() return value changed:
- Now returns
trueif element was added - Now returns
falseif element was removed
// v2.x
selected.invert(1); // returned result of add() or remove()
// v3.x
selected.invert(1); // returns true if added, false if removed
Made with 💙 for the Flutter community
Libraries
- collection_notifiers
- Collection classes with ChangeNotifier and ValueListenable support.