ListNotifier
Un List reactivo que notifica automáticamente a los listeners cuando su contenido cambia.
Descripción General
ListNotifier<T> es una implementación de List reactivo que:
- Extiende la interfaz estándar de Dart
List<T> - Implementa
ValueListenable<List<T>> - Notifica automáticamente a los listeners en las mutaciones
- Soporta transacciones para agrupar operaciones
- Proporciona modos de notificación configurables
Uso Básico
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]
}Creando un ListNotifier
Lista Vacía
final items = ListNotifier<String>();Con Datos Iniciales
final items = ListNotifier<String>(
data: ['item1', 'item2', 'item3'],
);Con Modo de Notificación
final items = ListNotifier<String>(
data: ['initial'],
notificationMode: CustomNotifierMode.normal,
);Con Igualdad Personalizada
class Product {
final String id;
final String name;
Product(this.id, this.name);
}
final products = ListNotifier<Product>(
notificationMode: CustomNotifierMode.normal,
customEquality: (a, b) => a.id == b.id, // Comparar solo por ID
);Operaciones Estándar de List
ListNotifier soporta todas las operaciones estándar de List con notificaciones automáticas:
Añadiendo Elementos
final items = ListNotifier<String>();
items.add('item1'); // Añadir un item
items.addAll(['item2', 'item3']); // Añadir múltiples items
items.insert(0, 'first'); // Insertar en índice
items.insertAll(1, ['a', 'b']); // Insertar múltiples en índiceEliminando Elementos
items.remove('item1'); // Eliminar por valor
items.removeAt(0); // Eliminar por índice
items.removeLast(); // Eliminar último item
items.removeRange(0, 2); // Eliminar rango
items.removeWhere((item) => item.startsWith('a')); // Eliminar condicionalmente
items.retainWhere((item) => item.length > 3); // Mantener solo coincidentes
items.clear(); // Eliminar todos los itemsActualizando Elementos
items[0] = 'updated'; // Actualizar por índice
items.setAll(0, ['a', 'b']); // Establecer múltiples empezando en índice
items.setRange(0, 2, ['x', 'y']); // Reemplazar rango
items.fillRange(0, 3, 'same'); // Llenar rango con mismo valorReordenando y Ordenando
items.sort(); // Ordenar items
items.sort((a, b) => a.compareTo(b)); // Ordenamiento personalizado
items.shuffle(); // Aleatorizar orden
items.swap(0, 1); // Intercambiar dos elementos (específico de ListNotifier)Cambiando Longitud
items.length = 10; // Crecer o encoger la listaOperaciones Especiales de ListNotifier
swap()
Intercambiar dos elementos por índice - solo notifica si los elementos son diferentes:
final items = ListNotifier<int>(data: [1, 2, 3]);
items.swap(0, 2); // ✅ Notifica: [3, 2, 1]
// Con modo normal y elementos iguales
final items2 = ListNotifier<int>(
data: [1, 1, 1],
notificationMode: CustomNotifierMode.normal,
);
items2.swap(0, 1); // ❌️ Sin notificación (elementos son iguales)Integración con Flutter
Con ValueListenableBuilder
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
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]),
),
);
}
}Modos de Notificación
ListNotifier soporta tres modos de notificación:
always (Predeterminado)
final items = ListNotifier<String>(
notificationMode: CustomNotifierMode.always,
);
items.add('item'); // ✅ Notifica
items[0] = 'item'; // ✅ Notifica (aunque el valor no cambió)
items.remove('xyz'); // ✅ Notifica (aunque no está en la lista)normal
final items = ListNotifier<String>(
notificationMode: CustomNotifierMode.normal,
);
items.add('item'); // ✅ Notifica
items[0] = 'item'; // ❌️ Sin notificación (valor sin cambios)
items.remove('xyz'); // ❌️ Sin notificación (no está en la lista)manual
final items = ListNotifier<String>(
notificationMode: CustomNotifierMode.manual,
);
items.add('item1'); // Sin notificación
items.add('item2'); // Sin notificación
items.notifyListeners(); // ✅ Notificación manualAprende más sobre modos de notificación →
Transacciones
Agrupa múltiples operaciones en una sola notificación:
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
}Aprende más sobre transacciones →
Valor Inmutable
El getter .value devuelve una vista no modificable:
final items = ListNotifier<String>(data: ['a', 'b', 'c']);
final immutableView = items.value;
print(immutableView); // [a, b, c]
// ❌️ Lanza UnsupportedError
// immutableView.add('d');
// ✅ Mutar a través del notifier
items.add('d'); // Funciona y notificaEsto asegura que todas las mutaciones pasen por el sistema de notificación.
Comportamiento de Operaciones Masivas
ListNotifier tiene un comportamiento especial para operaciones masivas:
Operaciones de Añadir/Insertar
Estas siempre notifican (incluso con entrada vacía) en todos los modos excepto manual:
final items = ListNotifier<String>(
notificationMode: CustomNotifierMode.normal,
);
items.addAll([]); // ✅ Notifica (aunque esté vacío)
items.insertAll(0, []); // ✅ Notifica (aunque esté vacío)
items.setAll(0, []); // ✅ Notifica (aunque esté vacío)
items.setRange(0, 0, []); // ✅ Notifica (aunque esté vacío)¿Por qué? Por razones de rendimiento - para evitar comparar todos los elementos. Estas operaciones se usan típicamente para carga masiva de datos.
Operaciones de Reemplazo
Estas solo notifican si ocurrieron cambios en modo normal:
final items = ListNotifier<String>(
data: ['a', 'a', 'a'],
notificationMode: CustomNotifierMode.normal,
);
items.fillRange(0, 3, 'a'); // ❌️ Sin notificación (valores sin cambios)
items.fillRange(0, 3, 'b'); // ✅ Notifica (valores cambiados)
items.replaceRange(0, 2, ['b', 'b']); // ❌️ Sin notificación (mismos valores)
items.replaceRange(0, 2, ['c', 'd']); // ✅ Notifica (valores cambiados)Operaciones Que Siempre Notifican
Algunas operaciones siempre activan el flag hasChanged:
shuffle()- El orden cambia aunque los valores nosort()- El orden probablemente cambiaswap()- Intercambiando elementos (pero verifica igualdad primero)setAll(),setRange()- Actualizaciones masivas
Casos de Uso
Lista de Tareas
class TodoListModel {
final todos = ListNotifier<Todo>();
void addTodo(String title) {
todos.add(Todo(id: generateId(), title: title, completed: false));
}
void toggleTodo(String id) {
final index = todos.indexWhere((t) => t.id == id);
if (index != -1) {
final todo = todos[index];
todos[index] = Todo(id: todo.id, title: todo.title, completed: !todo.completed);
}
}
void removeTodo(String id) {
todos.removeWhere((t) => t.id == id);
}
void reorderTodos(int oldIndex, int newIndex) {
todos.startTransAction();
final todo = todos.removeAt(oldIndex);
todos.insert(newIndex, todo);
todos.endTransAction();
}
}Mensajes de Chat
class ChatModel {
final messages = ListNotifier<Message>();
void addMessage(Message message) {
messages.add(message);
}
void loadHistory(List<Message> history) {
messages.startTransAction();
messages.clear();
messages.addAll(history);
messages.endTransAction();
}
void deleteMessage(String messageId) {
messages.removeWhere((m) => m.id == messageId);
}
}Resultados de Búsqueda
class SearchModel {
final results = ListNotifier<SearchResult>();
final isSearching = ValueNotifier<bool>(false);
Future<void> search(String query) async {
if (query.isEmpty) {
results.clear();
return;
}
isSearching.value = true;
try {
final newResults = await searchApi(query);
results.startTransAction();
results.clear();
results.addAll(newResults);
results.endTransAction();
} finally {
isSearching.value = false;
}
}
}Carrito de Compras
class ShoppingCart {
final items = ListNotifier<CartItem>(
notificationMode: CustomNotifierMode.normal,
customEquality: (a, b) => a.productId == b.productId,
);
void addItem(Product product) {
final existingIndex = items.indexWhere((item) => item.productId == product.id);
if (existingIndex != -1) {
// Actualizar cantidad
final existing = items[existingIndex];
items[existingIndex] = CartItem(
productId: existing.productId,
name: existing.name,
quantity: existing.quantity + 1,
price: existing.price,
);
} else {
// Añadir nuevo item
items.add(CartItem(
productId: product.id,
name: product.name,
quantity: 1,
price: product.price,
));
}
}
void removeItem(String productId) {
items.removeWhere((item) => item.productId == productId);
}
double get total => items.fold(0.0, (sum, item) => sum + (item.price * item.quantity));
}Consideraciones de Rendimiento
Memoria
ListNotifier tiene sobrecarga mínima comparada con una List regular:
- Extiende
DelegatingList(de package:collection) - Añade mecanismo de notificación de
ChangeNotifier - Pequeña sobrecarga para modo de notificación y flags de transacción
Notificaciones
Cada mutación dispara una notificación (a menos que esté en transacción o modo manual):
- Costo: O(n) donde n = número de listeners
- Optimización: Usa transacciones para operaciones masivas
- Mejor práctica: Mantén el conteo de listeners razonable (< 50)
Listas Grandes
Para listas muy grandes (1000+ items):
- Considera paginación en lugar de cargar todo de una vez
- Usa transacciones al añadir/eliminar muchos items
- Considera modo
normalsi tienes muchas operaciones sin efecto
// ❌️ Malo: 1000 notificaciones
for (var i = 0; i < 1000; i++) {
items.add(i);
}
// ✅ Bueno: 1 notificación
items.startTransAction();
for (var i = 0; i < 1000; i++) {
items.add(i);
}
items.endTransAction();
// ✅ Aún mejor: addAll
items.startTransAction();
items.addAll(List.generate(1000, (i) => i));
items.endTransAction();Combinando con Operators
Puedes encadenar operators de listen_it en un ListNotifier:
final todos = ListNotifier<Todo>();
// Reaccionar solo cuando cambia la longitud de la lista
final todoCount = todos.select<int>((list) => list.length);
// Filtrar a tareas incompletas
final incompleteTodos = todos.where((list) => list.any((t) => !t.completed));
// Debounce cambios rápidos
final debouncedTodos = todos.debounce(Duration(milliseconds: 300));
// Usar en widget
ValueListenableBuilder<int>(
valueListenable: todoCount,
builder: (context, count, _) => Text('$count todos'),
);Referencia de API
Constructor
ListNotifier({
List<T>? data,
CustomNotifierMode notificationMode = CustomNotifierMode.always,
bool Function(T, T)? customEquality,
})Propiedades
| Propiedad | Tipo | Descripción |
|---|---|---|
value | List<T> | Vista no modificable de la lista actual |
length | int | Número de elementos (setter dispara notificación) |
first | T | Primer elemento |
last | T | Último elemento |
isEmpty | bool | Si la lista está vacía |
isNotEmpty | bool | Si la lista tiene elementos |
Métodos
Todos los métodos estándar de List<T> más:
| Método | Descripción |
|---|---|
swap(int index1, int index2) | Intercambiar dos elementos |
startTransAction() | Comenzar transacción |
endTransAction() | Terminar transacción y notificar |
notifyListeners() | Notificar manualmente (útil con modo manual) |
Errores Comunes
1. Modificar la Vista .value
// ❌️ No intentes modificar el getter .value
final view = items.value;
view.add('item'); // ¡Lanza UnsupportedError!
// ✅ Modificar a través del notifier
items.add('item');2. Olvidar Transacciones
// ❌️ Muchas notificaciones
for (final item in newItems) {
items.add(item);
}
// ✅ Una sola notificación
items.startTransAction();
for (final item in newItems) {
items.add(item);
}
items.endTransAction();3. Transacciones Anidadas
// ❌️ Lanzará error de aserción
items.startTransAction();
items.add('a');
items.startTransAction(); // ¡ERROR!
// ✅ Terminar primera transacción antes de iniciar otra
items.startTransAction();
items.add('a');
items.endTransAction();
items.startTransAction();
items.add('b');
items.endTransAction();