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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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.
  14. 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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.
  14. 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.
  15. 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.
  16. 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.