RxDart vs FxDart
La misma tarea real, resuelta dos veces: RxDart a la izquierda, FxDart a la derecha. Las dos versiones se ejecutan en tu navegador e imprimen exactamente la misma salida — compara los dos modelos y decide qué modelo es en realidad tu problema.
Estas dos bibliotecas no son tanto rivales como
complementos. RxDart extiende el Stream
de Dart — un modelo push en el que el productor decide cuándo
llegan los valores, lo que hace naturales los operadores ligados al
reloj (debounceTime, combineLatest,
switchMap) y el multicast (BehaviorSubject).
FxDart trabaja sobre iterables — un modelo pull en el que el
consumidor decide cuándo pedir, lo que hace naturales la pereza, el
manejo tipado de errores y la concurrencia acotada en orden
(.concurrent(n)),
y convierte el backpressure en un no-problema: no tirar
es el backpressure. Los dos se encuentran en los puentes —
fromStream / toStream —
y varios de los ejemplos de abajo usan las dos bibliotecas juntas a
propósito.
Antes de la lista conviene nombrar una costumbre, porque es frecuente
y es un error. Stream trae un vocabulario de operadores
muy rico, así que resulta tentador recurrir a él envolviendo datos que
ya tienes en la mano —
Stream.fromIterable(pedidos) — solo para conseguir
map, where, distinct,
expand y una cadena fluida, y luego recuperar la
respuesta con un await al final. En ese problema no hay
nada asíncrono. Los valores están en memoria, la pregunta ya tiene
respuesta ahora mismo, y el await del final es la señal:
una pregunta síncrona convertida en un mecanismo de entrega para
tomar prestada su sintaxis. Lo que eso compra es vocabulario; lo que
cuesta es una suscripción, un giro del bucle de eventos y un paso de
entrega por cada elemento.
Los benchmarks de abajo le ponen número. En los 25 ejemplos cuya fuente ya está en memoria, compilados AOT y medidos con N = 1.000.000, el pipeline pull es más rápido en todos — una mediana de 2,7×, que sube a 88× cuando una búsqueda se corta antes de tiempo (#1, 78,9 ms vs 0,9 ms) y se mide en segundos enteros en los informes más pesados (#11, 3,4 s vs 175 ms). Donde el trabajo es genuinamente asíncrono, el mismo banco de pruebas encuentra los dos modelos a la par: en esos 16 ejemplos la diferencia mediana es de 1,05×, y la mayoría lleva insignia de empate. Ese contraste resume la sección en una línea: los streams no son lentos, pero hacer pasar datos síncronos por uno es pagar una entrega que nunca hizo falta.
Lo contrario merece decirse con la misma claridad: cuando un problema
va de verdad de eventos en el tiempo — entrada del usuario,
tickers, sockets — un stream es la forma correcta para él, y ningún
vocabulario de pipeline lo sustituye. FxDart lo
dice absorbiendo la idea: su capa de eventos
(fxEvents) pone
operadores push al estilo Rx — debounce, throttle, sample,
combineLatest, switchMap, race, un LiveValue — sobre
streams de Dart llanos, de modo que las parejas temporales de la
Parte 4 se encuentran como iguales, operador por operador.
RxDart sigue teniendo una jerarquía de clases Subject y sobrecargas
con sufijo de aridad combineLatest2…9; fxdart cubre los
trabajos sobre streams de Dart llanos sin chocar con rxdart
en el mismo fichero — ventanas, groupsBy en vivo,
shareReplay, debounce guiado por selector,
combine, las cuatro políticas pull
fromStream*. Lo que las parejas exponen es la otra
mitad de la historia — cuántas veces un problema que se resuelve con
un stream es en realidad un pipeline de datos disfrazado de
stream: una descarga acotada, una transformación por lotes, un
rastreo paginado. Para esos, la versión pull es más corta, ordenada,
tipada y no necesita ciclo de vida de suscripción alguno.
Gana FxDart — el modelo pull encaja mejor con este problema · Empate — ambos modelos lo expresan con limpieza · async — usa pipelines asíncronos
Las páginas cuya tarea tiene forma de rendimiento llevan además una sección de Benchmark — ambas implementaciones compiladas AOT y medidas en N=100 y un titular de N grande (1M para tareas síncronas; 10,000 donde cada elemento cruza el event loop). Los ejemplos de reloj de pared (#38–#46) deliberadamente no llevan benchmark: las ventanas de debounce y los ticks de sample miden el reloj, no el pipeline.
Dos perspectivas por ejemplo: la insignia rellena juzga el código — qué versión es más agradable de escribir y leer. La insignia ⚡ con contorno informa la medición — el ganador en tiempo de ejecución al mayor tamaño del benchmark, compilado AOT; la sección Benchmark de cada página tiene las barras completas.
Si solo vas a leer cinco, lee estos: #1 Primera transacción por encima del presupuesto · #11 Nivel de stock tras cada movimiento · #25 Reportar cada error de validación · #35 Obtener de 4 en 4, resultados en orden · #50 Una fuente, dos lectores independientes
Parte 1 · El mismo trabajo, ambas librerías
La zona común: transformaciones, filtros y cortes que existen en ambos vocabularios — cadenas pull y transformadores de stream, lado a lado.
- 1 Primera transacción por encima del presupuesto Empate ⚡ FxDart más rápido Encontrar la primera transacción por encima de 100 y detenerse — el firstWhere de Rx cancela la suscripción, el find de fxdart deja de tirar; ambos examinan solo 4 de 8.
- 2 Saldo acumulado de un feed de depósitos Empate ⚡ FxDart más rápido Plegar un feed de depósitos y retiradas en un saldo acumulado — el scan de Rx frente al scan de fxdart, una acumulación por movimiento en ambos lados.
- 3 Aplana los pedidos en líneas Empate ⚡ FxDart más rápido Aplanar cuatro pedidos en sus diez líneas pedido/sku — Stream.expand y el flatMap de fxdart son la misma palabra para uno-a-muchos, en orden de origen.
- 4 Suma los importes pares válidos Gana FxDart ⚡ FxDart más rápido Descarta los parseos fallidos, quédate con los pares y suma — un pipeline de Stream con un main asíncrono frente a una sola cadena pull síncrona sobre la misma lista fija.
- 5 Visitantes únicos, conservando la primera visita Empate ⚡ FxDart más rápido Deduplicar un registro de visitas a lo largo de todo el feed, conservando la primera visita de cada usuario — distinctUnique con equals+hashCode frente a uniqBy con una sola función de clave.
- 6 Los últimos tres errores Empate ⚡ FxDart más rápido Conservar las líneas ERROR e imprimir las tres últimas — takeLast espera el evento done, takeRight drena el iterable; ambos almacenan exactamente tres.
- 7 Una línea por defecto para un informe vacío Empate ⚡ FxDart más rápido Filtrar a una categoría sin coincidencias y aun así imprimir algo — defaultIfEmpty en el stream frente a ifEmpty en la cadena pull, la misma idea en ambos modelos.
- 8 Descarta los null, conserva los valores Empate ⚡ FxDart más rápido Limpiar un feed de sensor con nulos y formatear los supervivientes — whereNotNull es compact con otro nombre, y ambos estrechan double? a double estáticamente.
- 9 Salta las lecturas de calentamiento Empate ⚡ FxDart más rápido Descartar las lecturas bajas iniciales de una sonda y conservar todo lo demás — skipWhile y dropWhile son la misma compuerta de un solo sentido; hasta los operadores son del core.
- 10 Numera la lista de pasos Gana FxDart ⚡ FxDart más rápido Convertir seis pasos en líneas numeradas con 1. — los streams no tienen map con índice, así que Rx cuela un contador a través de scan; fxdart dice zipWithIndex.
Parte 2 · Ventanas, estado y orden
Búferes, pares, estado acumulado y adyacencia: donde empiezan a notarse las sutiles diferencias semánticas entre push y pull.
- 11 Nivel de stock tras cada movimiento Empate ⚡ FxDart más rápido Plegar recepciones y envíos de almacén en un nivel de stock acumulado y marcar los backorders — scan en ambos lados, semillas reproducidas de forma distinta.
- 12 Totales semanales de una serie diaria Gana FxDart ⚡ FxDart más rápido Enrollar 21 días de gasto en tres totales numerados por semana — bufferCount con scan reclutado como contador frente a chunk más zipWithIndex.
- 13 Conserva valores Y fallos en la auditoría Gana FxDart ⚡ FxDart más rápido Parsear ocho líneas de configuración donde tres fallan, imprimiendo los valores y el recuento de fallos — errores colados de vuelta como datos frente a una partición sencilla.
- 14 Marcadores de apertura y cierre Empate ⚡ FxDart más rápido Envolver un feed de sesión en líneas OPEN/CLOSE — startWith y endWith en el stream frente a prepend y append en la cadena pull.
- 15 Informa solo de los cambios de estado Empate ⚡ FxDart más rápido Colapsar un feed de salud repetitivo a una línea por racha — Stream.distinct frente a uniqAdjacent, con distinctUnique y uniq como los primos globales.
- 16 Sube en lotes de 4 Empate ⚡ FxDart más rápido Diez archivos pendientes, como mucho cuatro por petición — bufferCount(4) en el stream frente a chunk(4) en la cadena pull, con el último lote corto en ambos lados.
- 17 Gasto agrupado por categoría Gana FxDart ⚡ FxDart más rápido Totales por categoría en orden de primera aparición — un stream de GroupedStreams plegado y vuelto a mezclar frente a un groupBy que simplemente devuelve un Map.
- 18 Dos feeds, estrictamente en orden Empate ⚡ FxDart más rápido La cola del log de ayer seguida del log de hoy como una sola lista numerada — concatWith secuenciando suscripciones frente a concat secuenciando tirones.
- 19 Deltas entre ticks Empate ⚡ FxDart más rápido Cada tick de precio junto a su predecesor — pairwise en ambas bibliotecas, parejas como listas en el lado stream, records tipados en el lado pull.
- 20 Alinea el pronóstico con los datos reales Empate ⚡ FxDart más rápido Emparejar dos series fijas posición a posición e imprimir la diferencia de cada día — zipWith en streams frente a zip en iterables, la misma alineación en ambos casos.
- 21 Media móvil de tres lecturas Gana FxDart ⚡ FxDart más rápido Una media móvil sobre lecturas de sensor — bufferCount(3, 1) más un filtro de longitud para los parciales finales frente a windowed(3) diciendo exactamente lo que significa.
- 22 Toma hasta el marcador de apagado, inclusive Empate ⚡ FxDart más rápido Conservar cada evento hasta SHUTDOWN incluido y descartar los rezagados — takeWhileInclusive frente a takeUntilInclusive, el mismo corte en dos grafías.
- 23 Deduplica un feed paginado por id Empate ⚡ FxDart más rápido Aplanar tres páginas que se solapan y conservar cada id de producto una vez, en orden de llegada — expand más distinctUnique frente a flatMap más uniqBy.
- 24 La petición más rápida y la más lenta Empate async ⚡ FxDart más rápido Sondear ocho endpoints de forma asíncrona e imprimir la latencia mínima y máxima — reducciones que devuelven Future en ambos lados, una pasada fresca cada una.
Parte 3 · Errores y resiliencia
Un canal de errores sin tipos frente a errores como valores tipados: reintentos, timeouts, respaldos y la vida de los recursos en ambos modelos.
- 25 Reportar cada error de validación Gana FxDart ⚡ FxDart más rápido Cada regla rota por formulario, no solo la primera — valores de error llanos en una cadena síncrona vs un canal de errores que solo puede llevar un error y cerrarse.
- 26 Separar éxitos de fallos Gana FxDart async ⚡ FxDart más rápido Siete validaciones async, dos fallan — un try/catch por elemento que alimenta una partición tipada vs streams internos que devuelven el canal de errores a los datos.
- 27 Rendirse tras tres fallos Gana FxDart async ⚡ FxDart más rápido Contar fallos con scan y parar en el tercero, inclusive — un try/catch en el mapper vs convertir los errores en valores marcadores antes de que scan pueda verlos.
- 28 Reintentar con backoff creciente Gana FxDart async ⚡ FxDart más rápido Alargar la espera entre intentos — retryWhen mapea cada error a un stream de temporizador a mano frente a un hook delay que devuelve un Duration.
- 29 Reintentar el fetch inestable Empate async ⚡ FxDart más rápido Un fetch que falla dos veces y luego acierta — Rx.retry re-suscribe una fábrica de streams, el retry de fxdart re-ejecuta un Future, ambos en una llamada.
- 30 Acotar la lectura atascada Empate async ⚡ FxDart más rápido Un presupuesto de 150 ms sobre una lectura de sensor que se atasca — el timeout de stream vigila los huecos entre eventos, el timeout pull acota el tiempo demanda-a-elemento.
- 31 Reintentar cada fila inestable por separado Gana FxDart async ⚡ Misma velocidad Seis filas de importación inestables, dos intentos cada una, tres en vuelo — flatMap emite en orden de terminación, mapRetry bajo concurrent conserva el orden de origen.
- 32 La vida de un cursor alrededor de una lectura Empate async ⚡ Misma velocidad Abrir un cursor, leer cinco filas, garantizar el cierre — Rx.using alrededor de un stream vs usingAsync alrededor de un pull perezoso, dos ports de una misma idea.
- 33 Un precio, o el precio de lista Gana FxDart async ⚡ FxDart más rápido Un precio promocional donde exista, el precio de lista donde no — recuperación por elemento como streams internos frente a un try/catch justo al lado de la llamada.
- 34 Reanudar desde la caché cuando la fuente muere Empate async ⚡ Misma velocidad El feed en vivo muere tras tres actualizaciones — onErrorResumeNext conmuta a la cola cacheada frente a un bucle de pull explícito que alimenta concat.
Parte 4 · Concurrencia, tiempo y push
Donde los modelos se separan: concurrencia dirigida por la demanda en un lado, tiempo de reloj y multicast en el otro — incluidos los ejemplos donde RxDart es sencillamente la herramienta adecuada.
- 35 Obtener de 4 en 4, resultados en orden Gana FxDart async ⚡ FxDart más rápido Ocho fetches, cuatro en vuelo, impresos en orden de origen — mapConcurrent es ordenado por construcción; flatMap(maxConcurrent) debe etiquetar y reordenar.
- 36 El resultado más rápido primero Empate async ⚡ Misma velocidad Imprimir cada resultado en cuanto aterriza — el orden de terminación es el comportamiento nativo de flatMap, y fxdart lo iguala con un operador dedicado concurrentPool.
- 37 Un stream alimenta un pipeline tipado Empate async ⚡ Misma velocidad Un stream de logs en vivo fluye hacia un pipeline pull tipado a través de fxStream — conservar los warnings, ponerlos en mayúsculas y contar, a ambos lados del puente.
- 38 Aplicar debounce al buscador Empate async Esperar a que el tecleo se calme antes de buscar — debounceTime sobre el stream de eventos vs la misma cadena debounce en la capa de eventos de fxdart.
- 39 Un valor actual en vivo para lectores tardíos Empate async Un panel que se conecta tarde aún recibe la temperatura actual al instante — BehaviorSubject y LiveValue reemiten ambos el último valor, y luego transmiten en vivo.
- 40 Solo importa la búsqueda más nueva Empate async Una consulta más nueva abandona la búsqueda en vuelo — el mismo operador switchMap en ambos lados, rxdart y la cadena fxEvents de fxdart.
- 41 Sellar cada petición con la config más reciente Empate async Cada petición saliente lleva la versión de config vigente en ese instante — el mismo operador withLatestFrom en ambos lados, rxdart y fxEvents.
- 42 Una llamada cada 100 ms Empate async Cinco pings separados al menos 100 ms, probado con un Stopwatch monotónico — el interval de rx vs un delay llano en el mapper de una cadena pull secuencial.
- 43 Habilitar el envío cuando el formulario es válido Empate async Combinar los últimos valores de email y contraseña para gobernar el botón de envío — Rx.combineLatest2 vs combineLatest en la capa de eventos de fxdart.
- 44 Aplicar throttle al botón de refresco Empate async Dejar pasar un toque por ventana de 300 ms — throttleTime sobre el stream de toques vs la cadena throttle equivalente en la capa de eventos de fxdart.
- 45 Muestrear el medidor en cada tick de sondeo Empate async Leer el valor más reciente del medidor en cada tick de sondeo — un stream disparador de sample explícito en RxDart vs sampleOn en la capa de eventos de fxdart.
- 46 Poner a competir dos mirrors Empate async Dos mirrors compiten por un payload — Rx.race y FxEvents.race cancelan ambos el fetch perdedor en pleno vuelo, y ambos lo demuestran con un solo fetch completado.
- 47 Un pipeline alimenta a un consumidor de streams Empate async ⚡ Misma velocidad Un fetch ordenado con mapConcurrent entrega sus resultados a un consumidor de streams vía toStream — el puente cruzado en la otra dirección.
- 48 Rastrear páginas hasta agotarlas Gana FxDart async ⚡ Misma velocidad Pedir la página siguiente solo al estar listo — un cursor perezoso sin fin tirado bajo demanda vs un Rx.range suficientemente grande cancelado en la primera página vacía.
- 49 Cada llamada alimenta la siguiente Empate async ⚡ Misma velocidad Cuatro llamadas a la API donde cada respuesta siembra la petición siguiente — scan enhebra el estado por el pipeline; asyncMap se cierra sobre un token mutable.
- 50 Una fuente, dos lectores independientes Empate ⚡ FxDart más rápido Total y máximo de una fuente con efectos sin ejecutarla dos veces — un stream conectable vs fork compartiendo una sola pasada con buffer.