RxDart vs FxDart
The same real task, solved twice: RxDart on the left, FxDart on the right. Both versions run in your browser and print exactly the same output — compare the two models and decide which model your problem actually is.
These two libraries are not rivals so much as complements.
RxDart extends Dart's Stream — a push model where
the producer decides when values arrive, which makes wall-clock
operators (debounceTime, combineLatest,
switchMap) and multicast (BehaviorSubject)
natural. FxDart works on iterables — a pull model where the
consumer decides when to ask, which makes laziness, typed error
handling, and ordered bounded concurrency
(.concurrent(n))
natural, and makes backpressure a non-problem: not pulling
is the backpressure. The two meet at the bridges —
fromStream / toStream —
and several examples below use both libraries together on purpose.
One habit is worth naming before the list, because it is common and it
is a mistake. Stream carries a rich operator vocabulary,
so it is tempting to reach for it by wrapping data you already hold —
Stream.fromIterable(orders) — purely to get
map, where, distinct,
expand and a fluent chain, and then to
await the answer back at the end. Nothing in that problem
is asynchronous. The values are in memory, the question has an answer
right now, and the await at the end is the tell: a
synchronous question was converted into a delivery mechanism to
borrow its syntax. What that buys is vocabulary; what it costs is a
subscription, an event-loop turn, and a delivery step for every single
element.
The benchmarks below put a number on it. Across the 25 examples whose source is already in memory, AOT-compiled and measured at N = 1,000,000, the pull pipeline is faster in every one — a median of 2.7×, rising to 88× where a short-circuiting search stops early (#1, 78.9 ms vs 0.9 ms) and costing whole seconds on the heavier reports (#11, 3.4 s vs 175 ms). Where the work is genuinely asynchronous, the same harness finds the two models level: across those 16 examples the median gap is 1.05×, and most carry a tie badge. That contrast is the section in one line — streams are not slow, but putting synchronous data through one means paying for a delivery that never needed to happen.
The converse deserves saying just as plainly: when a problem really is
about events over time — user input, tickers, sockets — a
stream is the right shape for it, and no amount of pipeline vocabulary
replaces one. FxDart says so by absorbing the idea: its
events layer
(fxEvents) puts
Rx-style push operators — debounce, throttle, sample, combineLatest,
switchMap, race, a LiveValue — on plain Dart streams, so
Part 4's time-shaped pairs meet as equals, operator for operator.
RxDart still has a Subject class hierarchy and arity-suffixed
combineLatest2…9 overloads; fxdart covers the jobs
on plain Dart streams without colliding with rxdart in the same file —
windows, live groupsBy, shareReplay,
selector-driven debounce, combine, the four
fromStream* pull policies. What the pairs expose is the
other half of the story — how often a problem that gets solved with a
stream is really a data pipeline wearing a stream costume: a
bounded fetch, a batch transform, a paginated crawl. For those, the
pull version is shorter, ordered, typed, and needs no subscription
lifecycle at all.
FxDart wins — the pull model fits this problem better · Toss-up — both models express it cleanly · async — uses async pipelines
Pages whose task is throughput-shaped also carry a Benchmark section — both implementations AOT-compiled and measured at N=100 and a large-N headline (1M for synchronous tasks; 10,000 where every element crosses the event loop). The wall-clock examples (#38–#46) are deliberately not benchmarked: debounce windows and sample ticks measure the clock, not the pipeline.
各例には2つの視点があります。塗りつぶしバッジはコードの判定 — どちらが書きやすく読みやすいか。枠線のみの⚡バッジは計測結果 — 最大ベンチマークサイズでのAOTコンパイル実測の実行時間の勝者です。詳細なグラフは各ページのベンチマークセクションにあります。
If you only read five, read these: #1 First transaction over budget · #11 Stock level after each move · #25 Report every validation error · #35 Fetch 4 at a time, results in order · #50 One source, two independent readers
Part 1 · Same job, both libraries
The overlap: transforms, filters and slices that exist in both vocabularies — pull chains vs stream transformers, side by side.
- 1 First transaction over budget Toss-up ⚡ FxDartが高速 Find the first transaction over 100 and stop — Rx firstWhere cancels the subscription, fxdart find stops pulling; both examine only 4 of 8.
- 2 Running balance from a deposit feed Toss-up ⚡ FxDartが高速 Fold a feed of deposits and withdrawals into a running balance — Rx scan against fxdart scan, one accumulation per movement on both sides.
- 3 Flatten orders into lines Toss-up ⚡ FxDartが高速 Flatten four orders into their ten order/sku line items — Stream.expand and fxdart flatMap are the same word for one-to-many, in source order.
- 4 Total the valid even amounts FxDart wins ⚡ FxDartが高速 Drop failed parses, keep evens, sum — a Stream pipeline with an async main vs one synchronous pull chain over the same fixed list.
- 5 Unique visitors, first visit kept Toss-up ⚡ FxDartが高速 Dedupe a visit log across the whole feed, keeping each user's first visit — distinctUnique with equals+hashCode vs uniqBy with one key function.
- 6 The last three errors Toss-up ⚡ FxDartが高速 Keep the ERROR lines and print the last three — takeLast waits for the done event, takeRight drains the iterable; both buffer exactly three.
- 7 A default line for an empty report Toss-up ⚡ FxDartが高速 Filter to a category with no matches and still print something — defaultIfEmpty on the stream vs ifEmpty on the pull chain, the same idea in both models.
- 8 Drop the nulls, keep the values Toss-up ⚡ FxDartが高速 Clean a nullable sensor feed and format the survivors — whereNotNull is compact by another name, and both narrow double? to double statically.
- 9 Skip the warm-up readings Toss-up ⚡ FxDartが高速 Drop a probe's leading low readings, keep everything after — skipWhile and dropWhile are the same one-way gate; even the operators are core.
- 10 Number the checklist FxDart wins ⚡ FxDartが高速 Turn six steps into 1.-numbered lines — streams have no indexed map, so Rx smuggles a counter through scan; fxdart says zipWithIndex.
Part 2 · Windows, state & order
Buffers, pairs, running state and adjacency — where subtle semantic differences between push and pull start to show.
- 11 Stock level after each move Toss-up ⚡ FxDartが高速 Fold warehouse receipts and shipments into a running stock level and flag backorders — scan on both sides, seeds replayed differently.
- 12 Weekly totals from a daily series FxDart wins ⚡ FxDartが高速 Roll 21 days of spend into three week-numbered totals — bufferCount with scan drafted as a counter vs chunk plus zipWithIndex.
- 13 Keep values AND failures in the audit FxDart wins ⚡ FxDartが高速 Parse eight config lines where three fail, printing the values and the failure count — errors smuggled back as data vs a plain partition.
- 14 Open and close markers Toss-up ⚡ FxDartが高速 Wrap a session feed in OPEN/CLOSE lines — startWith and endWith on the stream vs prepend and append on the pull chain.
- 15 Report only status changes Toss-up ⚡ FxDartが高速 Collapse a repetitive health feed to one line per run — Stream.distinct vs uniqAdjacent, with distinctUnique and uniq as the global cousins.
- 16 Upload in batches of 4 Toss-up ⚡ FxDartが高速 Ten pending files, at most four per request — bufferCount(4) on the stream vs chunk(4) on the pull chain, with the short last batch on both sides.
- 17 Spend grouped by category FxDart wins ⚡ FxDartが高速 Per-category totals in first-seen order — a stream of GroupedStreams folded and merged back together vs a groupBy that just returns a Map.
- 18 Two feeds, strictly in order Toss-up ⚡ FxDartが高速 Yesterday's log tail followed by today's log as one numbered list — concatWith sequencing subscriptions vs concat sequencing pulls.
- 19 Deltas between ticks Toss-up ⚡ FxDartが高速 Each price tick with its predecessor — pairwise in both libraries, list pairs on the stream side, typed records on the pull side.
- 20 Align forecast with actuals Toss-up ⚡ FxDartが高速 Pair two fixed series position by position and print each day's difference — zipWith on streams vs zip on iterables, same alignment either way.
- 21 Three-reading moving average FxDart wins ⚡ FxDartが高速 A moving average over sensor readings — bufferCount(3, 1) plus a length filter for the trailing partials vs windowed(3) saying exactly what it means.
- 22 Take until the shutdown marker, inclusive Toss-up ⚡ FxDartが高速 Keep every event up to and including SHUTDOWN and drop the stragglers — takeWhileInclusive vs takeUntilInclusive, the same cut in two spellings.
- 23 Dedupe a paged feed by id Toss-up ⚡ FxDartが高速 Flatten three overlapping pages and keep each product id once, in arrival order — expand plus distinctUnique vs flatMap plus uniqBy.
- 24 Fastest and slowest request Toss-up async ⚡ FxDartが高速 Probe eight endpoints asynchronously and print the min and max latency — Future-returning reductions on both sides, one fresh pass each.
Part 3 · Errors & resilience
An untyped error channel vs errors as typed values — retry, timeout, fallbacks and resource lifetimes in both models.
- 25 Report every validation error FxDart wins ⚡ FxDartが高速 Every rule failure per form, not just the first — plain error values in a sync chain vs an error channel that can only carry one error and then close.
- 26 Split successes from failures FxDart wins async ⚡ FxDartが高速 Seven async validations, two fail — a per-item try/catch feeding a typed partition vs inner streams that turn the error channel back into data.
- 27 Give up after three failures FxDart wins async ⚡ FxDartが高速 Count failures with scan and stop at the third, inclusive — one try/catch in the mapper vs turning errors into marker values before scan can see them.
- 28 Retry with growing backoff FxDart wins async ⚡ FxDartが高速 Grow the wait between attempts — retryWhen maps each error to a timer stream by hand vs a delay hook that returns a Duration.
- 29 Retry the flaky fetch Toss-up async ⚡ FxDartが高速 A fetch that fails twice then succeeds — Rx.retry re-subscribes a stream factory, fxdart retry re-runs a Future, both in one call.
- 30 Bound the stalled read Toss-up async ⚡ FxDartが高速 A 150 ms budget on a stalling sensor read — stream timeout watches gaps between events, pull timeout bounds demand-to-item time.
- 31 Retry each flaky row independently FxDart wins async ⚡ 速度は同等 Six flaky import rows, two attempts each, three in flight — flatMap emits in completion order, mapRetry under concurrent keeps source order.
- 32 A cursor's lifetime around a read Toss-up async ⚡ 速度は同等 Open a cursor, read five rows, guarantee the close — Rx.using around a stream vs usingAsync around a lazy pull, two ports of one idea.
- 33 A price, or the list price FxDart wins async ⚡ FxDartが高速 A promo price where one exists, the list price where none does — per-item recovery as inner streams vs a try/catch right beside the call.
- 34 Resume from cache when the source dies Toss-up async ⚡ 速度は同等 The live feed dies after three updates — onErrorResumeNext swaps in the cached tail vs an explicit pull loop feeding concat.
Part 4 · Concurrency, time & push
Where the models split: demand-driven concurrency on one side, wall-clock and multicast on the other — including the examples where RxDart is simply the right tool.
- 35 Fetch 4 at a time, results in order FxDart wins async ⚡ FxDartが高速 Eight fetches, four in flight, printed in source order — mapConcurrent is ordered by construction; flatMap(maxConcurrent) must tag and re-sort.
- 36 Fastest result first Toss-up async ⚡ 速度は同等 Print each result the moment it lands — completion order is flatMap's native behavior, and fxdart matches it with a dedicated concurrentPool operator.
- 37 A stream feeds a typed pipeline Toss-up async ⚡ 速度は同等 A live log stream flows into a typed pull pipeline through fxStream — keep the warnings, uppercase them, and count, on both sides of the bridge.
- 38 Debounce the search box Toss-up async Wait for the typing to go quiet before searching — debounceTime on the event stream vs the same debounce chain in fxdart's events layer.
- 39 A live current value for late readers Toss-up async A dashboard that connects late still gets the current temperature instantly — BehaviorSubject and LiveValue both replay the latest, then stream live.
- 40 Only the newest search matters Toss-up async A newer query abandons the in-flight search — the same switchMap operator on both sides, rxdart and fxdart's fxEvents chain.
- 41 Stamp each request with the latest config Toss-up async Each outgoing request carries the config version current at that instant — the same withLatestFrom operator on both sides, rxdart and fxEvents.
- 42 One call every 100 ms Toss-up async Five pings at least 100 ms apart, proved by a monotonic Stopwatch — rx interval vs a plain delay in the mapper of a sequential pull chain.
- 43 Enable submit when the form is valid Toss-up async Combine the latest email and password values to drive the submit button — Rx.combineLatest2 vs combineLatest in fxdart's events layer.
- 44 Throttle the refresh button Toss-up async Let one tap through per 300 ms window — throttleTime on the tap stream vs the equivalent throttle chain in fxdart's events layer.
- 45 Sample the gauge on each poll tick Toss-up async Read the latest gauge value at each poll tick — an explicit sample trigger stream in RxDart vs sampleOn in fxdart's events layer.
- 46 Race two mirrors Toss-up async Two mirrors race for one payload — Rx.race and FxEvents.race both cancel the losing fetch mid-flight, and both prove it with one completed fetch.
- 47 A pipeline feeds a stream consumer Toss-up async ⚡ 速度は同等 An ordered mapConcurrent fetch hands its results to a stream consumer via toStream — the bridge crossed in the other direction.
- 48 Crawl pages until exhausted FxDart wins async ⚡ 速度は同等 Ask for the next page only when ready — an endless lazy cursor pulled on demand vs a big-enough Rx.range cancelled at the first empty page.
- 49 Each call feeds the next Toss-up async ⚡ 速度は同等 Four API calls where each response seeds the next request — scan threads the state through the pipeline; asyncMap closes over a mutable token.
- 50 One source, two independent readers Toss-up ⚡ FxDartが高速 Total and max from one side-effecting source without running it twice — a connectable stream vs two folds advancing on the same element.