skip to content

In RxDart, how do switchMap, exhaustMap and flatMap differ, and which fits a search box, a submit button and parallel poster downloads?

level: middleimportance: must knowfreq 50%

answer

  1. what happens to the running inner stream
  2. latest wins vs first wins vs all run
  3. flatMap's maxConcurrent queues the rest
  4. asyncExpand is Dart's sequential version
  5. 0.28 switchMap waits for cancel

basics

~20 s

switchMap cancels the running inner stream when a new event arrives, exhaustMap ignores new events until the running one completes, and flatMap runs them all concurrently, optionally capped by maxConcurrent. Search wants switchMap, a submit button exhaustMap, parallel downloads flatMap.

solid answer

~40 s

All three map each source event to an inner stream and flatten the results; they differ in what happens when an event arrives while an inner stream is still running. `switchMap` cancels its subscription to the old inner stream and listens to the new one — right for a search box, where only the latest query matters. `exhaustMap` ignores source events until the current inner stream completes — right for a submit or refresh button, so a double tap does not send two requests. `flatMap` subscribes to every inner stream at once and emits results as they arrive; its `maxConcurrent` parameter (null, meaning unlimited, by default) queues extra events — right for downloading many posters with a cap. For strictly one-after-another processing, Dart's own `asyncExpand` or `asyncMap` is enough; rxdart has no `concatMap` method.

code

dart · 12 lines
dart
import 'package:rxdart/rxdart.dart';

Future<void> main() async {
  // Three taps 50 ms apart; each call takes 120 ms.
  Stream<int> taps() =>
      Stream.periodic(const Duration(milliseconds: 50), (i) => i).take(3);
  Stream<String> call(int i) => Rx.timer('call $i', const Duration(milliseconds: 120));

  print(await taps().exhaustMap(call).toList()); // [call 0]
  print(await taps().switchMap(call).toList());  // [call 2]
  print(await taps().flatMap(call).toList());    // [call 0, call 1, call 2]
}

go deeper

for a junior

Remember the one-liner: switch drops the old, exhaust drops the new, flat drops nothing — and match each to search, a button and parallel downloads.

for a middle

Explain what each operator does with a running inner subscription, flatMap's maxConcurrent queueing, and why core asyncExpand covers sequential work.

for a senior

Justify choices in production terms: duplicate submissions, stale screens, server load from unbounded flatMap, and the fact that cancellation stops listening rather than stopping work.

for a principal

Decide where concurrency rules live — in rxdart pipelines, bloc event transformers or the repository — so a team applies one policy per interaction type.

## One question separates them `switchMap`, `exhaustMap` and `flatMap` are rxdart extension methods on `Stream`. Each takes a mapper `Stream<S> Function(T value)`, subscribes to the stream it returns (the **inner stream**), and forwards its events to one output stream. The only difference is the answer to: **a new source event arrives while an inner stream is still running — what now?** | Operator | New event while an inner stream runs | Output order | Movie-app use | |---|---|---|---| | `switchMap` | cancel the running inner subscription, listen to the new one | only the latest inner stream | search as you type | | `exhaustMap` | ignore the new event | only the first inner stream until it completes | "Add to watchlist" button, pull-to-refresh | | `flatMap` | listen to the new one too (up to `maxConcurrent`) | interleaved by arrival time | download many posters at once | | core `asyncExpand` | pause the source until the running one ends | strictly sequential | apply edits in order | ## switchMap: the latest wins `switchMap` keeps at most one inner subscription. When a new event arrives it calls the mapper for the new event, cancels the old subscription, and listens to the new inner stream. Events already emitted by the old one stay emitted; nothing more comes from it. - Since rxdart **0.28.0** it pauses the outer stream while the old subscription's `cancel()` completes, then resumes and listens to the next inner stream; an error thrown during that cancellation is forwarded to the output. - Cancelling a subscription does not cancel the `Future` behind `Stream.fromFuture`; the work finishes and its result is ignored. ## exhaustMap: the first wins `exhaustMap` also keeps at most one inner subscription, but it protects the running one: while it is active, new source events are **dropped** — not queued. When the inner stream completes, the next source event starts a new one. That is exactly the semantics of a button that must not fire twice: the second and third taps during a slow "add to watchlist" call simply vanish. It is also why `exhaustMap` is wrong for search — typing more while a request runs would be ignored, and the screen would show results for an outdated query. ## flatMap: everyone runs `flatMap` subscribes to every inner stream and merges their events in the order they arrive, so results can come back in a different order than the requests went out. - `maxConcurrent` is an `int?` parameter; **null means unlimited**. - When the limit is reached, further source events are **queued** in arrival order and started as running inner streams complete; nothing is dropped. - `flatMap(fetchPoster, maxConcurrent: 4)` downloads a page of posters four at a time. ## When Dart's core Stream is enough Dart's `Stream.asyncExpand` pauses the source, listens to each inner stream to completion, then resumes — a sequential, order-preserving flatten. `Stream.asyncMap` does the same for a `Future` per event. When the requirement is "one at a time, in order" (saving edits, replaying a queue), these core methods are enough, and rxdart does not add a `concatMap` method at all. ## How to choose, in order 1. Does only the newest request matter? Use **`switchMap`**. 2. Must a running operation finish before another may start, with extra triggers discarded? Use **`exhaustMap`**. 3. Should all of them run, possibly in parallel? Use **`flatMap`**, with `maxConcurrent` to protect the server or the device. 4. Should they run one at a time, none dropped? Use core **`asyncExpand`** or **`asyncMap`**. ```dart import 'package:rxdart/rxdart.dart'; void wire({ required Stream<String> queries, required Stream<void> addTaps, required Stream<String> posterIds, required Future<List<String>> Function(String) search, required Future<void> Function() addToWatchlist, required Future<List<int>> Function(String) fetchPoster, }) { queries.switchMap((q) => Stream.fromFuture(search(q))); addTaps.exhaustMap((_) => Stream.fromFuture(addToWatchlist())); posterIds.flatMap((id) => Stream.fromFuture(fetchPoster(id)), maxConcurrent: 4); } ``` The interview summary: **switch drops the old, exhaust drops the new, flat drops nothing**.

  • With flatMap(fetch, maxConcurrent: 2), what happens to the third event while two fetches are running?
    It is queued, not dropped. rxdart's flatMap stores events beyond the limit in a FIFO queue and starts the next one each time a running inner stream completes. Because inner streams finish independently, results may still arrive in a different order than the events.
  • Why is exhaustMap a poor choice for search-as-you-type?
    While a request is running, exhaustMap drops every new query. If the user keeps typing, the final query may never be sent, and the screen shows results for an outdated term. Search needs the opposite rule — the newest query replaces the running one — which is switchMap.

saying these in an interview costs you the question

  • exhaustMap queues the ignored events and runs them later.
  • flatMap preserves the order of the source events.
  • flatMap's maxConcurrent drops events beyond the limit.
  • switchMap cancels the Future that produced the old results.
  • RxDart provides concatMap for sequential processing.