Skip to content
listen_it logo

listen_it

Primitivas reactivas para Flutter - colecciones observables y operadores potentes para ValueListenable.

Descripción General

listen_it proporciona dos primitivas reactivas esenciales para el desarrollo en Flutter:

  1. Colecciones Reactivas - ListNotifier, MapNotifier, SetNotifier que automáticamente notifican a los listeners cuando su contenido cambia
  2. Operadores de ValueListenable - Métodos de extensión que te permiten transformar, filtrar, combinar y reaccionar a cambios de valor

Estas primitivas trabajan juntas para ayudarte a construir flujos de datos reactivos en tus apps Flutter sin generación de código o frameworks complejos.

Flujo de datos listen_it

Únete a nuestro servidor de Discord para soporte: https://discord.com/invite/Nn6GkYjzW

Instalación

Añade a tu pubspec.yaml:

yaml
dependencies:
  listen_it: ^5.2.0

Inicio Rápido

listen() - La Base

Te permite trabajar con un ValueListenable (y Listenable) como debería ser, instalando una función handler que se llama en cualquier cambio de valor y recibe el nuevo valor como argumento. Esto te da el mismo patrón que con Streams, haciéndolo natural y consistente.

dart
// Para ValueListenable<T>
ListenableSubscription listen(
  void Function(T value, ListenableSubscription subscription) handler
)

// Para Listenable
ListenableSubscription listen(
  void Function(ListenableSubscription subscription) handler
)
dart
void main() {
  final listenable = ValueNotifier<int>(0);

  // Basic listen - prints every value change
  final subscription = listenable.listen((x, _) => print(x));

  listenable.value = 1; // Prints: 1
  listenable.value = 2; // Prints: 2

  // Cancel subscription when done
  subscription.cancel();

  // This won't print anything (subscription cancelled)
  listenable.value = 3;
}

El subscription devuelto puede usarse para desactivar el handler. Como podrías necesitar desinstalar el handler desde dentro del mismo handler, recibes el objeto subscription como segundo parámetro de la función handler.

Esto es particularmente útil cuando quieres que un handler se ejecute solo una vez o un cierto número de veces:

dart
void runOnce() {
  final listenable = ValueNotifier<int>(0);

  // Run only once
  listenable.listen((x, subscription) {
    print('First value: $x');
    subscription.cancel();
  });
}

void runNTimes() {
  final listenable = ValueNotifier<int>(0);

  // Run exactly 3 times
  var count = 0;
  listenable.listen((x, subscription) {
    print('Value: $x');
    if (++count >= 3) subscription.cancel();
  });
}

Para Listenable regular (no ValueListenable), el handler solo recibe el parámetro subscription ya que no hay valor al cual acceder:

dart
void listenableExample() {
  final listenable = ChangeNotifier();
  listenable.listen((subscription) => print('Changed!'));
}

¿Por qué listen()?

  • Mismo patrón que Streams - API familiar si has usado Stream.listen()
  • Auto-cancelación - Los handlers pueden desuscribirse a sí mismos desde dentro del handler
  • Funciona fuera del árbol de widgets - Para lógica de negocio, servicios, efectos secundarios
  • Múltiples handlers - Instala múltiples handlers independientes en el mismo Listenable

Operadores de ValueListenable

Encadena operadores para transformar y reaccionar a cambios de valor:

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
}

Operadores Disponibles

OperadorCategoríaDescripción
listen()ListeningInstala handlers que reaccionan a cambios (patrón similar a Stream)
map()TransformaciónTransforma valores a diferentes tipos
select()TransformaciónReacciona solo cuando propiedades específicas cambian
where()FiltradoFiltra qué valores se propagan
debounce()Basado en TiempoRetrasa notificaciones hasta que los cambios paren
async()Basado en TiempoDifiere actualizaciones al siguiente frame
combineLatest()CombinaciónFusiona 2-6 ValueListenables
mergeWith()CombinaciónCombina cambios de valor de múltiples fuentes

Colecciones Reactivas

Versiones reactivas de List, Map y Set que implementan ValueListenable y automáticamente notifican a los listeners en mutaciones:

dart
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]
}

Úsalas con ValueListenableBuilder para UI reactiva:

dart
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]),
          ),
        );
      },
    );
  }
}

O con watchValue de watch_it para código más limpio:

dart
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]),
      ),
    );
  }
}

Eligiendo la Colección Correcta

ColecciónÚsala CuandoCasos de Uso Ejemplo
ListNotifier<T>El orden importa, duplicados permitidosListas de tareas, mensajes de chat, historial de búsqueda
MapNotifier<K,V>Necesitas búsquedas clave-valorPreferencias de usuario, cachés, datos de formulario
SetNotifier<T>Solo elementos únicos, pruebas rápidas de membresíaIDs de elementos seleccionados, filtros activos, etiquetas

Cuándo Usar Qué

Usa Operadores de ValueListenable Cuando:

  • ✅ Necesites transformar valores (map, select)
  • ✅ Necesites filtrar actualizaciones (where)
  • ✅ Necesites aplicar debounce a cambios rápidos (entradas de búsqueda)
  • ✅ Necesites combinar múltiples ValueListenables
  • ✅ Estés construyendo pipelines de transformación de datos

Usa Colecciones Reactivas Cuando:

  • ✅ Necesites una List, Map o Set que notifique listeners en mutaciones
  • ✅ Quieras actualizaciones automáticas de UI sin llamadas manuales a `notifyListeners()`
  • ✅ Estés construyendo listas reactivas, cachés o sets en tu capa de UI
  • ✅ Quieras agrupar múltiples operaciones en una sola notificación

Conceptos Clave

Colecciones Reactivas

Los tres tipos de colección (ListNotifier, MapNotifier, SetNotifier) extienden sus interfaces estándar de colección de Dart y añaden:

  • Notificaciones Automáticas - Cada mutación dispara listeners
  • Modos de Notificación - Controla cuándo se disparan las notificaciones (always, normal, manual)
  • Transacciones - Agrupa operaciones en notificaciones únicas
  • Valores Inmutables - Los getters .value devuelven vistas no modificables
  • Interfaz ValueListenable - Funciona con ValueListenableBuilder y watch_it

Aprende más sobre colecciones →

Operadores de ValueListenable

Los operadores crean cadenas de transformación:

  • Encadenables - Cada operador devuelve un nuevo ValueListenable
  • Inicialización Lazy - Las cadenas se suscriben solo cuando se añaden listeners
  • Suscripción Hot - Una vez suscritas, las cadenas permanecen suscritas
  • Tipado Seguro - Verificación completa de tipos en tiempo de compilación

Aprende más sobre operadores →

CustomValueNotifier

Un ValueNotifier con comportamiento de notificación y modos configurables.

Constructor

dart
CustomValueNotifier<T>(
  T initialValue, {
  CustomNotifierMode mode = CustomNotifierMode.normal,
  bool asyncNotification = false,
  void Function(Object error, StackTrace stackTrace)? onError,
})

Parámetros:

  • initialValue - El valor inicial
  • mode - Modo de notificación (por defecto: CustomNotifierMode.normal)
  • asyncNotification - Si es true, las notificaciones se difieren asíncronamente para evitar problemas de setState-durante-build
  • onError - Handler de errores opcional llamado cuando un listener lanza una excepción. Si no se proporciona, las excepciones se reportan vía FlutterError.reportError()

Uso Básico

dart
void main() {
  // Always mode - notify on every assignment
  final alwaysNotifier = CustomValueNotifier<int>(
    0,
    mode: CustomNotifierMode.always,
  );

  alwaysNotifier.addListener(() => print('Always: ${alwaysNotifier.value}'));

  alwaysNotifier.value = 42; // Notifies
  alwaysNotifier.value = 42; // Notifies again (same value)

  print('---');

  // Manual mode - only notify when you call notifyListeners()
  final manualNotifier = CustomValueNotifier<int>(
    0,
    mode: CustomNotifierMode.manual,
  );

  manualNotifier.addListener(() => print('Manual: ${manualNotifier.value}'));

  manualNotifier.value = 42; // No notification
  manualNotifier.value = 43; // No notification
  print('Current value: ${manualNotifier.value}'); // 43
  manualNotifier.notifyListeners(); // NOW listeners are notified

  print('---');

  // Normal mode (default) - notify only on value change
  final normalNotifier = CustomValueNotifier<int>(
    0,
    mode: CustomNotifierMode.normal,
  );

  normalNotifier.addListener(() => print('Normal: ${normalNotifier.value}'));

  normalNotifier.value = 42; // Notifies
  normalNotifier.value = 42; // No notification (same value)
  normalNotifier.value = 43; // Notifies
}

Modos de Notificación

CustomValueNotifier soporta tres modos vía el enum CustomNotifierMode:

  • normal (por defecto para CustomValueNotifier) - Solo notifica cuando el valor realmente cambia usando comparación ==
  • always - Notifica en cada asignación, incluso si el valor es el mismo
  • manual - Solo notifica cuando llamas explícitamente a notifyListeners()
dart
final counter = CustomValueNotifier<int>(
  0,
  mode: CustomNotifierMode.normal,  // por defecto
);

counter.value = 0;  // ❌️ Sin notificación (valor sin cambios)
counter.value = 1;  // ✅ Notifica (valor cambió)

Diferentes Valores por Defecto

CustomValueNotifier tiene por defecto el modo normal para ser un reemplazo directo de ValueNotifier, que solo notifica cuando el valor realmente cambia usando comparación ==.

Colecciones Reactivas (ListNotifier, MapNotifier, SetNotifier) tienen por defecto el modo always para asegurar actualizaciones de UI en cada operación, incluso cuando los objetos no sobrescriben ==.

Aprende más sobre modos de notificación →

Ejemplo del Mundo Real

Combinando operadores y colecciones para búsqueda reactiva:

dart
class SearchViewModel {
  final searchTerm = ValueNotifier<String>('');
  final results = ListNotifier<SearchResult>(data: []);

  SearchViewModel() {
    // Debounce search input to avoid excessive API calls
    searchTerm
        .debounce(const Duration(milliseconds: 300))
        .where((term) => term.length >= 3)
        .listen((term, _) => _performSearch(term));
  }

  Future<void> _performSearch(String term) async {
    final apiResults = await searchApi(term);

    // Use transaction to batch updates
    results.startTransAction();
    results.clear();
    results.addAll(apiResults);
    results.endTransAction();
  }
}

void main() async {
  final viewModel = SearchViewModel();

  // Listen to results
  viewModel.results.listen((items, _) {
    print('Search results: ${items.length} items');
  });

  // Simulate rapid typing
  viewModel.searchTerm.value = 'f';
  viewModel.searchTerm.value = 'fl';
  viewModel.searchTerm.value = 'flu';
  viewModel.searchTerm.value = 'flut';
  viewModel.searchTerm.value = 'flutter';

  // Only after 300ms pause, API is called and results updated
  await Future.delayed(Duration(milliseconds: 600));
}

Integración con el Ecosistema flutter_it

Con watch_it (¡Recomendado!)

watch_it v2.0+ proporciona caché automático de selectores, haciendo la creación de cadenas inline completamente segura:

dart
class SafeWatchItWidget extends WatchingWidget {
  @override
  Widget build(BuildContext context) {
    // ✅ SAFE: watch_it caches selectors by default
    // Chain created ONCE on first build, reused on subsequent builds
    final value = watchValue((Model m) => m.source.map((x) => x * 2));
    return Text('$value');
  }
}

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 →

Con get_it

Registra tus colecciones reactivas y cadenas en get_it para acceso global:

dart
void configureDependencies() {
  getIt.registerSingleton<ListNotifier<Todo>>(ListNotifier());
  getIt.registerLazySingleton(() => ValueNotifier<String>(''));
}

Aprende más sobre get_it →

Con command_it

command_it usa operadores de listen_it internamente para operaciones de ValueListenable:

dart
final command = Command.createAsync<String, void>(
  (searchTerm) async => performSearch(searchTerm),
  restriction: searchTerm.where((term) => term.length >= 3),
);

Aprende más sobre command_it →

Siguientes Pasos

Nombres Anteriores del Paquete

  • Previamente publicado como functional_listener (solo operadores)
  • Colecciones reactivas previamente publicadas como listenable_collections
  • Ambos están ahora unificados en listen_it v5.0+

Publicado bajo la Licencia MIT.