Operators
Los operators de ValueListenable son métodos de extensión que te permiten transformar, filtrar, combinar y reaccionar a cambios de valor de forma reactiva y componible.
Introducción
Las funciones de extensión en ValueListenable te permiten trabajar con ellos casi como streams síncronos. Cada operator devuelve un nuevo ValueListenable que se actualiza cuando la fuente cambia, permitiéndote construir pipelines de datos reactivos complejos a través del encadenamiento.
Conceptos Clave
Encadenables
Cada operator (excepto listen()) devuelve un nuevo ValueListenable, permitiéndote encadenar múltiples operators juntos:
void main() {
final intNotifier = ValueNotifier<int>(1);
// Chain multiple operators together
intNotifier
.where((x) => x.isEven) // Only allow even numbers
.map<String>((x) => x.toString()) // Convert to String
.listen((s, _) => print('Result: $s'));
intNotifier.value = 2; // Even - passes filter, converts to "2"
// Prints: Result: 2
intNotifier.value = 3; // Odd - blocked by filter
// No output
intNotifier.value = 4; // Even - passes filter, converts to "4"
// Prints: Result: 4
intNotifier.value = 5; // Odd - blocked by filter
// No output
intNotifier.value = 6; // Even - passes filter, converts to "6"
// Prints: Result: 6
}Tipado Seguro
Todos los operators mantienen verificación de tipos completa en tiempo de compilación:
final intNotifier = ValueNotifier<int>(42);
// El tipo se infiere: ValueListenable<String>
final stringNotifier = intNotifier.map<String>((i) => i.toString());
// Error de compilación si los tipos no coinciden
// final badNotifier = intNotifier.map<String>((i) => i); // ❌️ ErrorInicialización Eager
Por defecto, las cadenas de operators usan inicialización eager - se suscriben a su fuente inmediatamente, asegurando que .value siempre sea correcto incluso antes de añadir listeners. Esto soluciona problemas de valores obsoletos pero usa ligeramente más memoria.
final source = ValueNotifier<int>(5);
final mapped = source.map((x) => x * 2); // Se suscribe inmediatamente
print(mapped.value); // Siempre correcto: 10
source.value = 7;
print(mapped.value); // Actualizado inmediatamente: 14 ✅Para escenarios con limitaciones de memoria, pasa lazy: true para retrasar la suscripción hasta que se añada el primer listener:
final lazy = source.map((x) => x * 2, lazy: true);
// No se suscribe hasta que se llame a addListener()Ciclo de Vida de la Cadena
Una vez inicializadas (ya sea eager o después del primer listener), las cadenas de operators mantienen su suscripción a la fuente incluso cuando tienen cero listeners. Esta suscripción persistente es por diseño para eficiencia, pero puede causar fugas de memoria si las cadenas se crean inline en métodos build.
Mira la guía de mejores prácticas para patrones seguros.
Operators Disponibles
Transformación
Transforma valores a diferentes tipos o selecciona propiedades específicas:
- map() - Transforma valores usando una función
- select() - Reacciona solo cuando una propiedad seleccionada cambia
Filtrado
Controla qué valores se propagan a través de la cadena:
- where() - Filtra valores basándose en un predicado
Combinación
Fusiona múltiples ValueListenables juntos:
- combineLatest() - Combina dos ValueListenables
- mergeWith() - Fusiona múltiples ValueListenables
Basados en Tiempo
Controla el timing de la propagación de valores:
- debounce() - Solo propaga después de una pausa
- async() - Difiere actualizaciones al siguiente frame
Listening
Reacciona a cambios de valor:
- listen() - Instala una función handler que se llama en cada cambio de valor
Patrón de Uso Básico
Todos los operators siguen un patrón similar:
final source = ValueNotifier<int>(0);
// Crear cadena de operators
final transformed = source
.where((x) => x > 0)
.map<String>((x) => x.toString())
.debounce(Duration(milliseconds: 300));
// Usar con ValueListenableBuilder
ValueListenableBuilder<String>(
valueListenable: transformed,
builder: (context, value, _) => Text(value),
);
// O instalar un listener
transformed.listen((value, subscription) {
print('Valor cambió a: $value');
});Con watch_it
watch_it v2.0+ proporciona caché automático de selectores, haciendo la creación de cadenas inline completamente segura:
class UserInfoWidget extends WatchingWidget {
const UserInfoWidget({super.key});
@override
Widget build(BuildContext context) {
// watch_it v2.0+ caches the selector, so the chain is created only once
final userName = watchValue((Model m) =>
m.user.select<String>((u) => u.name).map((name) => name.toUpperCase()));
return Text('Hello, $userName!');
}
}El valor por defecto allowObservableChange: false cachea el selector, ¡así que la cadena se crea solo una vez!
Aprende más sobre integración con watch_it →
Patrones Comunes
Transformar Luego Filtrar
final intNotifier = ValueNotifier<int>(0);
intNotifier
.map((i) => i * 2) // Duplicar el valor
.where((i) => i > 10) // Solo valores > 10
.listen((value, _) => print(value));Seleccionar Luego Debounce
final userNotifier = ValueNotifier<User>(user);
userNotifier
.select<String>((u) => u.searchTerm) // Solo cuando searchTerm cambia
.debounce(Duration(milliseconds: 300)) // Esperar pausa
.listen((term, _) => search(term));Combinar Múltiples Fuentes
final source1 = ValueNotifier<int>(0);
final source2 = ValueNotifier<String>('');
source1
.combineLatest<String, Result>(
source2,
(int i, String s) => Result(i, s),
)
.listen((result, _) => print(result));Gestión de Memoria
Importante
Siempre crea cadenas fuera de métodos build o usa watch_it para caché automático.
❌️ NO HAGAS:
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: source.map((x) => x * 2), // ¡NUEVA CADENA EN CADA BUILD!
builder: (context, value, _) => Text('$value'),
);
}✅ HAZ:
// Opción 1: Crear cadena como campo
late final chain = source.map((x) => x * 2);
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: chain, // Mismo objeto en cada build
builder: (context, value, _) => Text('$value'),
);
}
// Opción 2: Usar watch_it (caché automático)
class MyWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final value = watchValue((Model m) => m.source.map((x) => x * 2));
return Text('$value');
}
}Lee la guía completa de mejores prácticas →