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

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

collection_notifiers #

pub CI codecov pub points license

Reactive List / Set / Map / Queue for Flutter. Mutate in place, rebuild on real change only. Ships matching flutter_hooks hooks for one-line widget integration.

final todos = useListNotifier<String>(['buy milk']);
todos.add('walk dog');   // rebuilds — new element
todos[0] = 'buy milk';   // silent — same value
todos[0] = 'buy eggs';   // rebuilds — value changed
todos.clear();           // rebuilds — was non-empty
todos.clear();           // silent — already empty

What it's for #

  • In-place collection state in a widget: selection toggles, todo lists, key/value settings, FIFO/LIFO queues.
  • Anywhere ValueNotifier<List<T>> would force a [...state, x] copy per mutation.

What it isn't #

  • Not a state container. Wrap with ChangeNotifierProvider (Provider) or expose via a Riverpod notifier when you need DI.
  • Not deep-reactive. Mutating an element in place (list[0].field = x) bypasses the equality check — use freezed or equatable for element types.
  • Not for a single value. Reach for stdlib ValueNotifier<T>.

Install #

flutter pub add collection_notifiers
import 'package:collection_notifiers/collection_notifiers.dart';

flutter_hooks is a runtime dependency — the hook variants ship with the package.


Pick the notifier #

Need Class Hook
Ordered, indexable, reorderable ListNotifier useListNotifier
Unique elements, selection toggles SetNotifier useSetNotifier
Key → value lookup MapNotifier useMapNotifier
FIFO/LIFO head-or-tail mutation QueueNotifier useQueueNotifier

Each class extends package:collection's DelegatingX and mixes in ChangeNotifier — drop-in replacement for the matching dart:core collection, plus ValueListenable<List<E>> / <Set<E>> / <Map<K, V>> / <Queue<E>>.


The hook owns the lifecycle: creates the notifier on first build, disposes on unmount, rebuilds the host widget on every mutation. Zero boilerplate.

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

  @override
  Widget build(BuildContext context) {
    final todos = useListNotifier<String>(['buy milk']);
    return Column(
      children: [
        FilledButton(
          onPressed: () => todos.add('walk dog'),
          child: const Text('Add'),
        ),
        for (final t in todos) Text(t),
      ],
    );
  }
}

initial is consumed once. To reset on a dependency change, scope the host widget under a different key so the hook re-mounts.

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

useListenable(notifier);

Without hooks #

If flutter_hooks is not on the project, fall back to ValueListenableBuilder — the notifier exposes itself as the value:

final todos = ListNotifier<String>(['buy milk']);

ValueListenableBuilder<List<String>>(
  valueListenable: todos,
  builder: (context, items, _) => Column(
    children: [for (final t in items) Text(t)],
  ),
);

Dispose todos yourself — ChangeNotifierProvider / State.dispose / ref.onDispose.


Notification contract #

Mutating methods call notifyListeners() only when the underlying collection actually changes.

final tags = SetNotifier<String>({'flutter', 'dart'});
tags.add('rust');     // notifies — new element
tags.add('rust');     // silent — already present
tags.remove('rust');  // notifies — element removed
tags.remove('rust');  // silent — wasn't there
tags.clear();         // notifies — set drained
tags.clear();         // silent — already empty

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

The per-method strategy — length-delta diff, equality-guarded single-slot, containsKey disambiguation for nullable values — is documented in each method's dartdoc.

Exceptions #

  • ListNotifier.sort / shuffle on length > 1 always notify. Verifying order-preservation costs O(n) per call and defeats the optimisation budget. Lists of length 0 or 1 short-circuit silently.
  • MapNotifier.addEntries uses a length-only check: re-inserting an existing key with a different value mutates the map but does not notify. Use operator []= or addAll when per-key value-diff matters.

Patterns #

Multi-select with SetNotifier #

final selected = useSetNotifier<int>();

CheckboxListTile(
  value: selected.contains(id),
  onChanged: (_) => selected.invert(id),
  title: Text('Item $id'),
);

invert(e) toggles presence — returns true if added, false if removed.

Settings with MapNotifier #

final settings = useMapNotifier<String, Object>({
  'darkMode': false,
  'fontSize': 14,
});

settings['darkMode'] = !(settings['darkMode']! as bool);
settings['fontSize'] = 14;   // silent — already 14

Todos with ListNotifier #

final todos = useListNotifier<Todo>();

todos.add(Todo(title: 'learn Flutter'));
todos.removeWhere((t) => t.completed);

// reorder
final item = todos.removeAt(oldIndex);
todos.insert(newIndex, item);

// sort always notifies on length > 1 — see "Exceptions"
todos.sort((a, b) => a.priority.compareTo(b.priority));

Riverpod #

final todosProvider = ChangeNotifierProvider((ref) {
  return ListNotifier<String>(['initial']);
});

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todosProvider);
    return Column(children: [for (final t in todos) Text(t)]);
  }
}

Agent setup #

A bundled skill ships at skills/flutter-collection-notifiers/SKILL.md. Vendor it into your agent's skill directory (~/.claude/skills/flutter-collection-notifiers/, or the Cursor / AntiGravity equivalent). It teaches the agent to pick the right notifier, wire the matching hook, respect dispose discipline, and stop replacing the collection instead of mutating it.


Pitfalls #

notifier = ListNotifier([...]) — reassigning kills listeners notifier..clear()..addAll([...]) — mutate in place
Custom element types with default == / hashCode freezed / equatable so equality checks can see real changes
useListNotifier(seed) with a fresh seed per rebuild expecting a reset initial is consumed once — change the host widget's key to reset
StatefulWidget holding a notifier without disposing Use the matching hook, or override dispose and call notifier.dispose()
Expecting addEntries to fire on value change for an existing key Use operator []= / addAlladdEntries is length-only

Migration 1.x → 2.x #

Breaking: SetNotifier.invert(e) return value:

// v1.x — returned whichever of add()/remove() ran
// v2.x — true if element was added, false if removed
selected.invert(1);

License #

MIT.

6
likes
160
points
74
downloads

Documentation

API reference

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
Contributing

Topics

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

License

MIT (license)

Dependencies

collection, flutter, flutter_hooks

More

Packages that depend on collection_notifiers