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

Libraries

collection_notifiers
Collection classes with ChangeNotifier and ValueListenable support.