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

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

collection_notifiers #

Pub Version CI codecov pub points 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 #

The package ships dedicated flutter_hooks for every collection type — useListNotifier, useSetNotifier, useMapNotifier, useQueueNotifier. Each one creates the notifier, disposes it on unmount, and rebuilds the host widget on every change. Zero boilerplate.

import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection_notifiers/collection_notifiers.dart';

class TodoList extends HookWidget {
  const TodoList({super.key});

  @override
  Widget build(BuildContext context) {
    // 🪄 Creates, disposes, and subscribes in one call.
    final todos = useListNotifier<String>(['Buy milk']);

    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (context, index) => Text(todos[index]),
    );
  }
}

Use custom hooks:

  • ✂️ Zero Boilerplate: No ValueListenableBuilder nesting, no dispose override
  • 🔄 Auto-Dispose: The hook owns the lifecycle
  • 🧼 Cleaner Code: Reads like synchronous code
  • 🧩 Composable: Easy to combine with other hooks

If the notifier is owned upstream (Riverpod, parent widget), subscribe without recreating it:

class TodoList extends HookWidget {
  const TodoList({super.key, required this.notifier});

  final ListNotifier<String> notifier;

  @override
  Widget build(BuildContext context) {
    useListenable(notifier);
    return ListView.builder(
      itemCount: notifier.length,
      itemBuilder: (context, i) => Text(notifier[i]),
    );
  }
}

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() on a list of length > 1 always notify — even when the order didn't actually change. Verifying order-preservation would cost O(n) per call and defeats the smart-notification budget. Lists of length 0 or 1 short-circuit silently.


🤖 Agent setup #

Coding with an LLM? Drop skills/flutter-collection-notifiers/SKILL.md into your agent's skill directory (~/.claude/skills/flutter-collection-notifiers/, or the Cursor / AntiGravity equivalent). The skill teaches the agent to pick the right notifier, wire the matching custom hook, respect dispose discipline, and stop replacing the collection instead of mutating it.


📂 Where the custom hooks live #

The hooks ship at lib/src/ui/ — one file per type (use_list_notifier.dart, use_set_notifier.dart, use_map_notifier.dart, use_queue_notifier.dart). If your project prefers a different layout (lib/src/hooks/, lib/src/widgets/, etc.), fork the package or re-export the hooks from your own path — the part directives in lib/collection_notifiers.dart are the only place to update.


📖 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
0
points
119
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

License

unknown (license)

Dependencies

collection, flutter, flutter_hooks

More

Packages that depend on collection_notifiers