collection_notifiers 2.0.1 copy "collection_notifiers: ^2.0.1" to clipboard
collection_notifiers: ^2.0.1 copied to clipboard

Reactive collection wrappers (List, Map, Set, Queue) with ChangeNotifier and ValueListenable support for efficient Flutter UI rebuilds.

collection_notifiers #

Pub Version 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. Connect to your UI #

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 ValueListenableBuilder nesting
  • 🔄 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 useValueListenable hook 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 true if element was added
  • Now returns false if 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
6
likes
160
points
268
downloads

Publisher

verified publishermehmetesen.com

Weekly Downloads

Reactive collection wrappers (List, Map, Set, Queue) with ChangeNotifier and ValueListenable support for efficient Flutter UI rebuilds.

Homepage
Repository (GitHub)
View/report issues

Topics

#state-management #flutter #collections #change-notifier #value-listenable

Documentation

API reference

License

MIT (license)

Dependencies

collection, flutter

More

Packages that depend on collection_notifiers