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:
- Colecciones Reactivas - ListNotifier, MapNotifier, SetNotifier que automáticamente notifican a los listeners cuando su contenido cambia
- 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.
Únete a nuestro servidor de Discord para soporte: https://discord.com/invite/Nn6GkYjzW
Instalación
Añade a tu pubspec.yaml:
dependencies:
listen_it: ^5.2.0Inicio 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.
// Para ValueListenable<T>
ListenableSubscription listen(
void Function(T value, ListenableSubscription subscription) handler
)
// Para Listenable
ListenableSubscription listen(
void Function(ListenableSubscription subscription) handler
)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:
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:
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:
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
| Operador | Categoría | Descripción |
|---|---|---|
| listen() | Listening | Instala handlers que reaccionan a cambios (patrón similar a Stream) |
| map() | Transformación | Transforma valores a diferentes tipos |
| select() | Transformación | Reacciona solo cuando propiedades específicas cambian |
| where() | Filtrado | Filtra qué valores se propagan |
| debounce() | Basado en Tiempo | Retrasa notificaciones hasta que los cambios paren |
| async() | Basado en Tiempo | Difiere actualizaciones al siguiente frame |
| combineLatest() | Combinación | Fusiona 2-6 ValueListenables |
| mergeWith() | Combinación | Combina 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:
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:
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:
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 Cuando | Casos de Uso Ejemplo |
|---|---|---|
| ListNotifier<T> | El orden importa, duplicados permitidos | Listas de tareas, mensajes de chat, historial de búsqueda |
| MapNotifier<K,V> | Necesitas búsquedas clave-valor | Preferencias de usuario, cachés, datos de formulario |
| SetNotifier<T> | Solo elementos únicos, pruebas rápidas de membresía | IDs 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
.valuedevuelven vistas no modificables - Interfaz ValueListenable - Funciona con
ValueListenableBuildery 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
CustomValueNotifier<T>(
T initialValue, {
CustomNotifierMode mode = CustomNotifierMode.normal,
bool asyncNotification = false,
void Function(Object error, StackTrace stackTrace)? onError,
})Parámetros:
initialValue- El valor inicialmode- Modo de notificación (por defecto:CustomNotifierMode.normal)asyncNotification- Si es true, las notificaciones se difieren asíncronamente para evitar problemas de setState-durante-buildonError- Handler de errores opcional llamado cuando un listener lanza una excepción. Si no se proporciona, las excepciones se reportan víaFlutterError.reportError()
Uso Básico
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()
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 ==.
Ejemplo del Mundo Real
Combinando operadores y colecciones para búsqueda reactiva:
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:
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:
void configureDependencies() {
getIt.registerSingleton<ListNotifier<Todo>>(ListNotifier());
getIt.registerLazySingleton(() => ValueNotifier<String>(''));
}Con command_it
command_it usa operadores de listen_it internamente para operaciones de ValueListenable:
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_itv5.0+