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); // ❌️ ErrorCiclo de Vida de la Cadena
Las cadenas de operators se suscriben a su fuente al crearse, se desconectan cuando se elimina su último listener y se reconectan con el siguiente. Mientras están desconectadas, .value se deriva del valor actual de la fuente al leerlo, así que nunca está obsoleto:
final source = ValueNotifier<int>(5);
final mapped = source.map((x) => x * 2);
print(mapped.value); // 10
source.value = 7;
print(mapped.value); // 14 ✅ - correcto con o sin listenersPasa lazy: true para retrasar la primera suscripción hasta que se añada el primer listener - una optimización pura de memoria, .value sigue siendo correcto:
final lazy = source.map((x) => x * 2, lazy: true);
print(lazy.value); // derivado al leer, aún sin suscripciónMantén las Transformaciones Puras
La transformación puede ejecutarse al leer mientras la cadena está desconectada, así que las funciones de transformación no deben tener efectos secundarios.
Mira la guía de mejores prácticas para el ciclo de vida completo.
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 cachea el selector, así que crear una cadena inline en un selector es seguro - se crea una vez por instancia de widget y se desconecta de su fuente cuando el widget se dispone:
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!');
}
}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
Crea las cadenas una sola vez - fuera de métodos build o dentro de un selector de watch_it. Una cadena creada en build() es un objeto nuevo en cada reconstrucción; se desconecta en cuanto pierde sus listeners, así que no hay fuga, pero hace trabajo innecesario.
❌️ 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 →