Modos de Notificación
Controla cuándo se notifica a los listeners con tres modos de notificación: always, normal y manual.
Descripción General
Todas las colecciones reactivas (ListNotifier, MapNotifier, SetNotifier) soportan tres modos de notificación via el enum CustomNotifierMode:
| Modo | Comportamiento | Usar Cuando |
|---|---|---|
| always | Notifica en cada operación, incluso si el valor no cambia | Predeterminado para colecciones - previene confusión de actualización de UI |
| normal | Solo notifica cuando el valor realmente cambia (usando == o igualdad personalizada) | Predeterminado para CustomValueNotifier - optimizando rendimiento |
| manual | Sin notificaciones automáticas - llama a notifyListeners() manualmente | Control completo sobre notificaciones |
Por qué always es el predeterminado para colecciones: Los usuarios esperan que la UI se reconstruya cuando realizan una operación (como añadir un item). Si la operación no dispara una notificación, podría sorprender a los usuarios cuando la UI no se actualiza como esperado. El modo always asegura comportamiento consistente independientemente de si los objetos sobrescriben ==.
Predeterminados Diferentes
Colecciones Reactivas (ListNotifier, MapNotifier, SetNotifier) usan modo always por defecto.
CustomValueNotifier usa modo normal por defecto para ser un reemplazo directo de ValueNotifier, coincidiendo con su comportamiento de solo notificar cuando el valor realmente cambia.
Uso Básico
void main() {
// Normal mode - only notify on actual changes
final normalCart = SetNotifier<String>(
data: {},
notificationMode: CustomNotifierMode.normal,
);
normalCart.listen((items, _) => print('Normal: $items'));
normalCart.add('item1'); // ✅ Notifies (new item)
normalCart.add('item1'); // ❌ No notification (already exists)
print('---');
// Always mode - notify on every operation (default)
final alwaysCart = SetNotifier<String>(
data: {},
notificationMode: CustomNotifierMode.always,
);
alwaysCart.listen((items, _) => print('Always: $items'));
alwaysCart.add('item1'); // ✅ Notifies
alwaysCart.add('item1'); // ✅ Notifies (even though already exists)
print('---');
// Manual mode - you control when to notify
final manualCart = SetNotifier<String>(
data: {},
notificationMode: CustomNotifierMode.manual,
);
manualCart.listen((items, _) => print('Manual: $items'));
manualCart.add('item1'); // No automatic notification
manualCart.add('item2'); // No automatic notification
manualCart.notifyListeners(); // ✅ Single notification for both adds
}Modo always (Predeterminado)
Notifica a los listeners en cada operación, independientemente de si el valor realmente cambió.
Por Qué Es el Predeterminado
class User {
final String name;
final int age;
User(this.name, this.age);
// ❌️ Sin sobrescritura de igualdad - cada instancia es única
}
final users = ListNotifier<User>(); // Predeterminado: modo always
users.listen((list, _) => print('Users: ${list.length}'));
final user1 = User('John', 25);
users.add(user1); // ✅ Notifica
users.add(user1); // ✅ Notifica (referencia duplicada, pero UI se actualiza)Problema con modo normal aquí: Sin sobrescribir ==, Dart usa igualdad de referencia. Aunque sea la misma referencia de objeto, los usuarios podrían esperar que la UI se actualice cuando llaman a .add().
Solución: Usar modo always por defecto para que la UI siempre se actualice cuando se realizan operaciones. Esto coincide con las expectativas del usuario y previene confusión.
Cuándo Usar always
- ✅ Opción predeterminada - funciona correctamente independientemente de la implementación de igualdad
- ✅ Cuando quieres que la UI se actualice en cada operación
- ✅ Cuando los objetos no sobrescriben el operador `==`
- ✅ Al depurar - ver cada operación
final items = ListNotifier<String>(
notificationMode: CustomNotifierMode.always,
);
items.add('item'); // ✅ Notifica
items.add('item'); // ✅ Notifica (aunque sea duplicado)
items[0] = 'item'; // ✅ Notifica (aunque el valor no cambió)Modo normal
Solo notifica a los listeners cuando el valor realmente cambia, usando comparación == (o función de igualdad personalizada).
Uso Básico
final items = ListNotifier<String>(
notificationMode: CustomNotifierMode.normal,
);
items.listen((list, _) => print('Changed: $list'));
items.add('item1'); // ✅ Notifica (item nuevo)
items.add('item2'); // ✅ Notifica (item nuevo)
items[0] = 'item1'; // ❌️ Sin notificación (mismo valor)
items.remove('xyz'); // ❌️ Sin notificación (item no está en la lista)Con Igualdad Personalizada
Proporciona una función de comparación personalizada para objetos complejos:
class Product {
final String id;
final String name;
final double price;
Product(this.id, this.name, this.price);
}
final products = ListNotifier<Product>(
notificationMode: CustomNotifierMode.normal,
customEquality: (a, b) => a.id == b.id, // Comparar solo por ID
);
final product1 = Product('1', 'Widget', 9.99);
final product2 = Product('1', 'Widget Pro', 14.99); // Mismo ID, nombre diferente
products.add(product1);
products[0] = product2; // ❌️ Sin notificación (mismo ID según customEquality)Operaciones Masivas en Modo normal
Diferentes operaciones masivas tienen diferente comportamiento de notificación:
Operaciones de añadir/insertar - Siempre notifican (incluso con entrada vacía):
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)Operaciones de reemplazo - Solo notifican si ocurrieron cambios:
items.fillRange(0, 2, 'a'); // Solo notifica si los valores cambiaron
items.replaceRange(0, 2, []); // Solo notifica si los valores cambiaronCuándo Usar normal
- ✅ Optimización de rendimiento - reducir notificaciones innecesarias
- ✅ Los objetos sobrescriben el operador `==` correctamente
- ✅ Tienes lógica de igualdad personalizada
- ✅ Las operaciones sin efecto no deberían disparar actualizaciones de UI
class Todo {
final String id;
final String title;
final bool completed;
Todo(this.id, this.title, this.completed);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Todo &&
runtimeType == other.runtimeType &&
id == other.id &&
title == other.title &&
completed == other.completed;
@override
int get hashCode => id.hashCode ^ title.hashCode ^ completed.hashCode;
}
final todos = ListNotifier<Todo>(
notificationMode: CustomNotifierMode.normal,
);
final todo1 = Todo('1', 'Buy milk', false);
todos.add(todo1); // ✅ Notifica
todos[0] = todo1; // ❌️ Sin notificación (mismo objeto)
todos[0] = Todo('1', 'Buy milk', false); // ❌️ Sin notificación (igual por ==)Modo manual
Sin notificaciones automáticas - debes llamar a notifyListeners() manualmente.
Uso Básico
final items = ListNotifier<String>(
notificationMode: CustomNotifierMode.manual,
);
items.listen((list, _) => print('Manual notification: $list'));
items.add('item1'); // Sin notificación
items.add('item2'); // Sin notificación
items.add('item3'); // Sin notificación
items.notifyListeners(); // ✅ Una sola notificación para las 3 adicionesCuándo Usar manual
- ✅ Operaciones complejas que requieren múltiples pasos
- ✅ Quieres control explícito sobre cuándo se disparan las notificaciones
- ✅ Agrupar operaciones para rendimiento (¡usa transacciones en su lugar!)
- ✅ Notificaciones condicionales basadas en lógica personalizada
final cart = ListNotifier<Product>(
notificationMode: CustomNotifierMode.manual,
);
void updateCart(List<Product> newProducts) {
cart.clear();
cart.addAll(newProducts);
// Solo notificar si el carrito no está vacío
if (cart.isNotEmpty) {
cart.notifyListeners();
}
}manual vs Transacciones
Para agrupar operaciones, las transacciones son usualmente mejores que el modo manual:
❌️ Con modo manual:
final items = ListNotifier<String>(
notificationMode: CustomNotifierMode.manual,
);
// Debes recordar llamar a notifyListeners()
items.add('a');
items.add('b');
items.notifyListeners(); // ¡Fácil de olvidar!✅ Con transacciones (cualquier modo):
final items = ListNotifier<String>(); // Cualquier modo funciona
items.startTransAction();
items.add('a');
items.add('b');
items.endTransAction(); // Notificación garantizadaAprende más sobre transacciones →
Tabla de Comparación
| Operación | always | normal | manual |
|---|---|---|---|
add(newItem) | ✅ Notifica | ✅ Notifica | ❌️ Sin notificación |
add(duplicate) (Set) | ✅ Notifica | ❌️ Sin notificación | ❌️ Sin notificación |
[index] = sameValue | ✅ Notifica | ❌️ Sin notificación | ❌️ Sin notificación |
remove(nonExistent) | ✅ Notifica | ❌️ Sin notificación | ❌️ Sin notificación |
addAll([]) (vacío) | ✅ Notifica | ✅ Notifica | ❌️ Sin notificación |
fillRange() sin cambio | ✅ Notifica | ❌️ Sin notificación | ❌️ Sin notificación |
notifyListeners() | ✅ Notifica | ✅ Notifica | ✅ Notifica |
Eligiendo el Modo Correcto
Árbol de Decisión
¿Necesitas control completo sobre las notificaciones?
├─ SÍ → Usa modo manual
│ (¡Pero considera transacciones en su lugar!)
└─ NO → ¿Tus objetos sobrescriben ==?
├─ SÍ → Usa modo normal
│ (Reduce notificaciones innecesarias)
└─ NO/INSEGURO → Usa modo always (predeterminado)
(Previene confusión de actualización de UI)Recomendaciones por Tipo de Colección
ListNotifier:
- Predeterminado:
always- Los usuarios esperan actualizaciones de UI en cada operación - Usa
normalsi: La lista contiene tipos de valor con==apropiado (String, int, etc.) - Usa
manualsi: Tienes operaciones de lote complejas
MapNotifier:
- Predeterminado:
always- Opción segura para cualquier tipo de valor - Usa
normalsi: Tienes comparación de clave personalizada o igualdad de valor - Usa
manualsi: Estás construyendo el map en etapas
SetNotifier:
- Predeterminado:
always- Previene confusión al añadir duplicados - Usa
normalsi: Quieres sin notificación al añadir items existentes - Usa
manualsi: Estás cargando datos masivamente
Ejemplos del Mundo Real
Ejemplo 1: Carrito de Compras (modo normal)
class CartItem {
final String id;
final String name;
final int quantity;
final double price;
CartItem(this.id, this.name, this.quantity, this.price);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CartItem &&
id == other.id &&
name == other.name &&
quantity == other.quantity &&
price == other.price;
@override
int get hashCode => Object.hash(id, name, quantity, price);
}
final cart = ListNotifier<CartItem>(
notificationMode: CustomNotifierMode.normal,
);
// Solo notifica cuando el carrito realmente cambia
void updateItemQuantity(String id, int newQuantity) {
final index = cart.indexWhere((item) => item.id == id);
if (index != -1) {
final item = cart[index];
cart[index] = CartItem(item.id, item.name, newQuantity, item.price);
// Solo notifica si la cantidad realmente cambió
}
}Ejemplo 2: Items Seleccionados (modo normal)
final selectedIds = SetNotifier<String>(
notificationMode: CustomNotifierMode.normal,
);
selectedIds.listen((ids, _) => print('Selection changed: $ids'));
selectedIds.add('item1'); // ✅ Notifica
selectedIds.add('item1'); // ❌️ Sin notificación (ya está en el set)
selectedIds.add('item2'); // ✅ NotificaEjemplo 3: Datos de Formulario (modo manual)
final formData = MapNotifier<String, String>(
notificationMode: CustomNotifierMode.manual,
);
void loadFormData(Map<String, String> data) {
formData.clear();
formData.addAll(data);
// Solo notificar después de que todos los datos estén cargados
formData.notifyListeners();
}
void validateAndSubmit() {
if (isValid(formData)) {
formData.notifyListeners(); // Notificar solo si es válido
submitForm(formData);
}
}Consideraciones de Rendimiento
Modo always
- Pros: Simple, predecible, previene bugs de UI
- Contras: Puede notificar más a menudo de lo necesario
- Impacto: Usualmente insignificante a menos que sean miles de actualizaciones/segundo
Modo normal
- Pros: Reduce notificaciones innecesarias, mejor rendimiento
- Contras: Requiere implementación apropiada de
==, ligeramente más complejo - Impacto: Puede reducir significativamente reconstrucciones con operaciones sin efecto frecuentes
Modo manual
- Pros: Control máximo, puede agrupar múltiples operaciones
- Contras: Fácil olvidar notificaciones, más propenso a errores
- Impacto: Mejor rendimiento cuando se usa correctamente