collection_notifiers

Pub Version Build Status codecov License: MIT

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 true if element was added
  • Now returns false if 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.