Skip to content

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:

dart
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:

dart
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); // ❌️ Error

Inicializació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.

dart
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:

dart
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:

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:

dart
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:

dart
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

dart
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

dart
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

dart
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:

dart
Widget build(BuildContext context) {
  return ValueListenableBuilder(
    valueListenable: source.map((x) => x * 2), // ¡NUEVA CADENA EN CADA BUILD!
    builder: (context, value, _) => Text('$value'),
  );
}

✅ HAZ:

dart
// 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 →

Próximos Pasos

Publicado bajo la Licencia MIT.