listenable_collections 3.0.0 copy "listenable_collections: ^3.0.0" to clipboard
listenable_collections: ^3.0.0 copied to clipboard

A collection of Dart collections that behave like ValueNotifier if their data changes.

📢 Important Notice

This is the last release under the listenable_collections name. In future versions, these features will be integrated into the listen_it package.

For more information, visit flutter-it.dev

listenable_collections #

Reactive Dart collections that integrate with Flutter's ValueListenableBuilder and watch_it

Extend your Lists, Maps, and Sets with automatic change notifications - perfect for reactive UI updates in Flutter without complex state management solutions.

✨ Features #

  • 🔔 Automatic Notifications - Collections notify listeners on every mutation
  • 📦 Three Collection Types - ListNotifier, MapNotifier, SetNotifier
  • 🎯 Three Notification Modes - Fine-grained control over when notifications fire
  • Transaction Support - Batch multiple operations into a single notification
  • 🔒 Immutability - Value getters return unmodifiable views
  • 🎨 Flutter Integration - Works with ValueListenableBuilder or watch_it

📦 Installation #

dependencies:
  listenable_collections: ^1.0.0

🚀 Quick Start #

Creating Reactive Collections #

Simply wrap your collection type with a notifier:

// Instead of:
final items = <String>[];

// Use:
final items = ListNotifier<String>();

// With initial data:
final items = ListNotifier<String>(data: ['item1', 'item2']);

All standard collection methods work as expected - the difference is they now notify listeners!

Integration with Flutter #

Option 1: Using ValueListenableBuilder (built-in)

class TodoListWidget extends StatelessWidget {
  final todos = ListNotifier<String>();

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<List<String>>(
      valueListenable: todos,
      builder: (context, items, _) {
        return ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) => Text(items[index]),
        );
      },
    );
  }
}

Option 2: Using watch_it (cleaner syntax)

// Add watch_it dependency first
dependencies:
  watch_it: ^1.0.0

class TodoListWidget extends WatchingWidget {
  final todos = ListNotifier<String>();

  @override
  Widget build(BuildContext context) {
    final items = watch(todos).value;
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) => Text(items[index]),
    );
  }
}

🎯 Notification Modes #

Choose when your UI should update:

// Normal mode - only notify on actual changes
final cart = SetNotifier<String>(
  notificationMode: CustomNotifierMode.normal,
);
cart.add('item1');    // ✅ Notifies (new item)
cart.add('item1');    // ❌ No notification (already exists)

// Always mode - notify on every operation (default)
final cart = SetNotifier<String>(
  notificationMode: CustomNotifierMode.always,
);
cart.add('item1');    // ✅ Notifies
cart.add('item1');    // ✅ Notifies (even though already exists)

// Manual mode - you control when to notify
final cart = SetNotifier<String>(
  notificationMode: CustomNotifierMode.manual,
);
cart.add('item1');
cart.add('item2');
cart.notifyListeners(); // ✅ Single notification for both adds

Why the default is always? If users haven't overridden == operator, they expect UI updates even when setting the "same" value.

📊 Choosing the Right Collection #

Collection Use When Example Use Case
ListNotifier<T> Order matters, duplicates allowed Todo list, chat messages, search history
MapNotifier<K,V> Need key-value lookups User preferences, caches, form data
SetNotifier<T> Unique items only, fast membership tests Selected item IDs, active filters, tags

⚡ Batch Updates with Transactions #

Avoid unnecessary rebuilds by batching operations:

final products = ListNotifier<Product>();

// Without transaction: 3 UI updates
products.add(product1);
products.add(product2);
products.add(product3);

// With transaction: 1 UI update
products.startTransAction();
products.add(product1);
products.add(product2);
products.add(product3);
products.endTransAction();

🔍 Real-World Examples #

Shopping Cart Manager #

class ShoppingCart {
  final items = MapNotifier<String, CartItem>();

  void addItem(Product product) {
    items[product.id] = CartItem(product: product, quantity: 1);
  }

  void updateQuantity(String productId, int quantity) {
    if (quantity <= 0) {
      items.remove(productId);
    } else {
      items[productId] = items[productId]!.copyWith(quantity: quantity);
    }
  }

  double get total => items.values
      .fold(0.0, (sum, item) => sum + item.price * item.quantity);
}

// In your widget with watch_it:
class CartSummary extends WatchingWidget {
  final ShoppingCart cart;

  @override
  Widget build(BuildContext context) {
    final itemCount = watch(cart.items).value.length;
    return Text('$itemCount items in cart');
  }
}

Multi-Select with Sets #

class FilterManager {
  final selectedFilters = SetNotifier<String>(
    notificationMode: CustomNotifierMode.normal,
  );

  void toggleFilter(String filter) {
    if (selectedFilters.contains(filter)) {
      selectedFilters.remove(filter);
    } else {
      selectedFilters.add(filter);
    }
  }

  bool isSelected(String filter) => selectedFilters.contains(filter);
}

// In your widget:
ValueListenableBuilder<Set<String>>(
  valueListenable: filterManager.selectedFilters,
  builder: (context, selected, _) {
    return Wrap(
      children: availableFilters.map((filter) {
        return FilterChip(
          label: Text(filter),
          selected: selected.contains(filter),
          onSelected: (_) => filterManager.toggleFilter(filter),
        );
      }).toList(),
    );
  },
)

Message Queue with Transactions #

class MessageQueue {
  final messages = ListNotifier<Message>(
    notificationMode: CustomNotifierMode.normal,
  );

  Future<void> loadBatch(List<Message> newMessages) async {
    messages.startTransAction();
    messages.clear();
    messages.addAll(newMessages);
    messages.endTransAction(); // Single rebuild
  }
}

🎨 Collection-Specific Features #

ListNotifier #

  • Index-based access: list[0] = newValue
  • Custom equality: Only notify when values truly differ
  • Reordering: Built-in swap(index1, index2) method
  • Smart operations: fillRange, replaceRange compare values in normal mode

MapNotifier #

  • Smart putIfAbsent: In always mode, notifies even when key exists
  • Null-safe: remove() only notifies if key existed
  • Custom equality: Compare values, not just references

SetNotifier #

  • Duplicate detection: add() returns bool (was it actually added?)
  • No custom equality: Uses Set's built-in == and hashCode (less confusing)
  • Set operations: union(), intersection(), difference() are read-only (don't notify)

🆚 When to Use This vs Alternatives #

Use listenable_collections when:

  • ✅ You have simple local state (a list, map, or set)
  • ✅ You want minimal boilerplate
  • ✅ You're already comfortable with ValueNotifier
  • ✅ You need fine-grained control (notification modes, transactions)

Consider alternatives when:

  • ❌ You need global state management → Use Provider, Riverpod, or get_it with watch_it
  • ❌ You need computed/derived state → Use Provider or Riverpod
  • ❌ You need state persistence → Combine with shared_preferences or state management solution
  • ❌ You have complex state logic → Use BLoC, Riverpod, or Redux patterns

🔧 Advanced Features #

Custom Equality (List and Map only) #

// Only notify when user ID changes (ignore other fields)
final users = ListNotifier<User>(
  customEquality: (a, b) => a.id == b.id,
  notificationMode: CustomNotifierMode.normal,
);

users[0] = updatedUser; // No notification if ID is the same

Note: SetNotifier intentionally doesn't support custom equality to avoid confusion with Set's inherent == and hashCode behavior.

Immutable Snapshots #

final list = ListNotifier<int>(data: [1, 2, 3]);

// Get immutable snapshot
final snapshot = list.value; // UnmodifiableListView<int>

// This throws UnsupportedError:
snapshot.add(4); // ❌ Can't modify the snapshot

// To modify, use the notifier directly:
list.add(4); // ✅ This works and notifies listeners

Integration with watch_it #

For cleaner syntax and better ergonomics:

class MyWidget extends WatchingWidget {
  final items = ListNotifier<String>();

  @override
  Widget build(BuildContext context) {
    // Clean, readable syntax
    final itemList = watch(items).value;

    return Column(
      children: itemList.map((item) => Text(item)).toList(),
    );
  }
}

📚 API Documentation #

Detailed API documentation is available on pub.dev:

🤝 Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License #

MIT License - see the LICENSE file for details.

6
likes
130
points
173
downloads

Publisher

unverified uploader

Weekly Downloads

A collection of Dart collections that behave like ValueNotifier if their data changes.

Repository (GitHub)
View/report issues

Documentation

API reference

License

unknown (license)

Dependencies

collection, flutter, functional_listener

More

Packages that depend on listenable_collections