Introducción a Colecciones
Las colecciones reactivas notifican automáticamente a los listeners cuando su contenido cambia, facilitando la construcción de UIs reactivas sin llamadas manuales a notifyListeners().
¿Qué Son las Colecciones Reactivas?
listen_it proporciona tres tipos de colecciones reactivas que implementan ValueListenable:
- ListNotifier<T> - List reactivo con notificaciones automáticas
- MapNotifier<K,V> - Map reactivo con notificaciones automáticas
- SetNotifier<T> - Set reactivo con notificaciones automáticas
Cada tipo de colección extiende la interfaz estándar de colección de Dart (List, Map, Set) y añade capacidades reactivas.
Ejemplo Rápido
void main() {
final items = ListNotifier<String>(data: []);
// Listen to changes - gets notified on every mutation
items.listen((list, _) {
print('List changed: $list');
});
items.add('first item');
// Prints: List changed: [first item]
items.add('second item');
// Prints: List changed: [first item, second item]
items.addAll(['third', 'fourth']);
// Prints: List changed: [first item, second item, third, fourth]
items.removeAt(1);
// Prints: List changed: [first item, third, fourth]
items[0] = 'updated first';
// Prints: List changed: [updated first, third, fourth]
}Características Clave
1. Notificaciones Automáticas
Cada operación de mutación notifica automáticamente a los listeners:
final items = ListNotifier<String>();
items.listen((list, _) => print('List changed: $list'));
items.add('item1'); // ✅ Notifica
items.addAll(['a', 'b']); // ✅ Notifica
items[0] = 'updated'; // ✅ Notifica
items.removeAt(0); // ✅ Notifica2. Modos de Notificación
Controla cuándo se disparan las notificaciones con tres modos:
- always (predeterminado) - Notifica en cada operación, incluso si el valor no cambia
- normal - Solo notifica cuando el valor realmente cambia (usando
==o igualdad personalizada) - manual - Sin notificaciones automáticas, llama a
notifyListeners()manualmente
Aprende por qué el predeterminado notifica siempre →
3. Transacciones
Agrupa múltiples operaciones en una sola notificación:
final items = ListNotifier<int>();
items.startTransAction();
items.add(1);
items.add(2);
items.add(3);
items.endTransAction(); // Una sola notificación para las 3 adicionesAprende más sobre transacciones →
4. Valores Inmutables
El getter .value devuelve una vista no modificable:
final items = ListNotifier<String>(data: ['a', 'b']);
final immutableView = items.value; // UnmodifiableListView
// immutableView.add('c'); // ❌️ Lanza UnsupportedErrorEsto asegura que todas las mutaciones pasen por el sistema de notificación.
5. Interfaz ValueListenable
Todas las colecciones implementan ValueListenable, por lo que funcionan con:
ValueListenableBuilder- Widget reactivo estándar de Flutterwatch_it- Para código reactivo más limpio- Cualquier otra solución de gestión de estado que observe Listenables
- Todos los operators de listen_it - Encadena transformaciones en colecciones
Casos de Uso
ListNotifier - Colecciones Ordenadas
Usa cuando el orden importa y se permiten duplicados:
- Listas de tareas
- Historial de mensajes de chat
- Resultados de búsqueda
- Feeds de actividad
- Items vistos recientemente
MapNotifier - Almacenamiento Clave-Valor
Usa cuando necesitas búsquedas rápidas por clave:
- Preferencias de usuario
- Datos de formularios
- Cachés
- Configuraciones
- Mapeos de ID a objeto
final preferences = MapNotifier<String, dynamic>(
data: {'theme': 'dark', 'fontSize': 14},
);
preferences.listen((map, _) => savePreferences(map));
preferences['theme'] = 'light'; // ✅ NotificaSetNotifier - Colecciones Únicas
Usa cuando necesitas items únicos y pruebas de membresía rápidas:
- IDs de items seleccionados
- Filtros activos
- Etiquetas
- Categorías únicas
- Permisos de usuario
final selectedIds = SetNotifier<String>(data: {});
selectedIds.listen((set, _) => print('Selection changed: $set'));
selectedIds.add('item1'); // ✅ Notifica
selectedIds.add('item1'); // No se añade duplicado (comportamiento de Set)Integración con Flutter
Con ValueListenableBuilder
Enfoque estándar de Flutter:
class TodoListWidget extends StatelessWidget {
const TodoListWidget(this.todos, {super.key});
final ListNotifier<String> todos;
@override
Widget build(BuildContext context) {
// ListNotifier's value type is List<String>, not ListNotifier<String>
return ValueListenableBuilder<List<String>>(
valueListenable: todos,
builder: (context, items, _) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(
title: Text(items[index]),
),
);
},
);
}
}Con watch_it (¡Recomendado!)
Más limpio y conciso:
class TodoListWidget extends WatchingWidget {
const TodoListWidget(this.todos, {super.key});
final ListNotifier<String> todos;
@override
Widget build(BuildContext context) {
final items = watch(todos).value;
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(
title: Text(items[index]),
),
);
}
}Eligiendo la Colección Correcta
| Colección | Cuándo Usar | Ejemplo |
|---|---|---|
| ListNotifier<T> | El orden importa, duplicados permitidos | Listas de tareas, historial de mensajes |
| MapNotifier<K,V> | Necesitas búsquedas clave-valor | Configuraciones, cachés, datos de formulario |
| SetNotifier<T> | Items únicos, pruebas de membresía rápidas | IDs seleccionados, filtros, etiquetas |
Patrones Comunes
Inicializar con Datos
Todas las colecciones aceptan datos iniciales:
final items = ListNotifier<String>(data: ['a', 'b', 'c']);
final prefs = MapNotifier<String, int>(data: {'count': 42});
final tags = SetNotifier<String>(data: {'flutter', 'dart'});Escuchar Cambios
Usa .listen() para efectos secundarios fuera del árbol de widgets:
final cart = ListNotifier<Product>();
cart.listen((products, _) {
final total = products.fold(0.0, (sum, p) => sum + p.price);
print('Cart total: \$$total');
});Agrupar Operaciones con Transacciones
Mejora el rendimiento agrupando actualizaciones:
void main() {
final products = ListNotifier<Product>(data: []);
// Listen to changes
products.listen((list, _) => print('Products updated: ${list.length} items'));
final product1 = Product(id: '1', name: 'Widget', price: 9.99);
final product2 = Product(id: '2', name: 'Gadget', price: 19.99);
final product3 = Product(id: '3', name: 'Doohickey', price: 29.99);
print('--- Without transaction: 3 notifications ---');
products.add(product1); // Notification 1
products.add(product2); // Notification 2
products.add(product3); // Notification 3
products.clear();
print('\n--- With transaction: 1 notification ---');
products.startTransAction();
products.add(product1); // No notification
products.add(product2); // No notification
products.add(product3); // No notification
products.endTransAction(); // Single notification for all 3 adds
}Elegir Modo de Notificación
El predeterminado es always porque los usuarios esperan que la UI se reconstruya en cada operación. Usar el modo normal podría sorprender a los usuarios si la UI no se actualiza cuando realizan una operación (como añadir un item que ya existe), pero puedes optimizar con normal cuando entiendes las compensaciones:
final items = ListNotifier<String>(
notificationMode: CustomNotifierMode.normal,
);
items.add('item1'); // ✅ Notifica
items.add('item1'); // ❌️ Sin notificación (duplicado en set/map, o sin cambio)¿Por Qué Colecciones Reactivas?
Sin Colecciones Reactivas
class TodoList extends ValueNotifier<List<Todo>> {
TodoList() : super([]);
void addTodo(Todo todo) {
value.add(todo);
notifyListeners(); // Notificación manual
}
void removeTodo(int index) {
value.removeAt(index);
notifyListeners(); // Notificación manual
}
void updateTodo(int index, Todo todo) {
value[index] = todo;
notifyListeners(); // Notificación manual
}
}Con ListNotifier
final todos = ListNotifier<Todo>();
todos.add(todo); // ✅ Notificación automática
todos.removeAt(index); // ✅ Notificación automática
todos[index] = updatedTodo; // ✅ Notificación automáticaBeneficios:
- ✅ Menos código repetitivo
- ✅ APIs estándar de List/Map/Set
- ✅ Notificaciones automáticas
- ✅ Soporte de transacciones para agrupación