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. Connect to your UI
Option A: Using flutter_hooks (Recommended)
This package is designed to work perfectly with flutter_hooks. The useValueListenable hook automatically handles subscription and disposal:
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection_notifiers/collection_notifiers.dart';
class TodoList extends HookWidget {
final todos = ListNotifier<String>(['Buy milk']);
@override
Widget build(BuildContext context) {
// ๐ช Automatically rebuilds when collection changes
final items = useValueListenable(todos);
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => Text(items[index]),
);
}
}
Why we recommend hooks:
- โ๏ธ Zero Boilerplate: No
ValueListenableBuildernesting - ๐ Auto-Dispose: Subscriptions are managed automatically
- ๐งผ Cleaner Code: Reads like synchronous code
- ๐งฉ Composable: Easy to combine with other hooks
Option B: Using ValueListenableBuilder
If you're not using hooks, use the standard 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
Pro Tip: As mentioned in the Quick Start, we strongly recommend using flutter_hooks via the
useValueListenablehook for the cleanest, most idiomatic code.
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 1.x
Breaking change: SetNotifier.invert() return value changed in 2.0.0:
- Now returns
trueif element was added - Now returns
falseif element was removed
// v1.x
selected.invert(1); // returned result of add() or remove()
// v2.x
selected.invert(1); // returns true if added, false if removed
Libraries
- collection_notifiers
- Collection classes with ChangeNotifier and ValueListenable support.