In a Flutter app, when do Dart's core Stream methods suffice, and which RxDart operators have no core Stream equivalent?
answer
- extension methods on Stream
- core: transform one stream
- RxDart: time and cancellation
- RxDart: combining several sources
- a dependency is a cost
basics
~20 sCore Stream already maps, filters, de-duplicates and runs async work in order (map, where, distinct, asyncMap, asyncExpand). RxDart earns its dependency for timing (debounceTime, throttleTime), cancel-or-ignore flattening (switchMap, exhaustMap, flatMap) and combining sources (combineLatest2, zip2, merge, withLatestFrom).
solid answer
~40 sDart's `Stream` already covers single-stream transformation: `map`, `where`, `distinct`, `take`, `skip`, `handleError`, `timeout`, plus `asyncMap` and `asyncExpand` for sequential asynchronous work. If a feature only needs those, adding rxdart buys nothing. rxdart adds, as extension methods on `Stream`, three families core lacks: **time-based rate limiting** (`debounceTime`, `throttleTime`), **flattening that cancels or ignores** (`switchMap`, `exhaustMap`) or runs concurrently (`flatMap` with `maxConcurrent`), and **multi-source combination** (`Rx.combineLatest2`, `Rx.zip2`, `Rx.merge`, `withLatestFrom`), plus conveniences such as `startWith`. Because they are extensions returning plain `Stream`s, you can mix core and rxdart calls in one chain and adopt rxdart only where a screen needs it — a debounced movie search, say.
code
dart · 7 linesimport 'package:rxdart/rxdart.dart';
Stream<int> sampleChain(Stream<int> source) => source
.where((n) => n.isEven) // core
.debounceTime(const Duration(milliseconds: 200)) // rxdart
.distinct() // core
.startWith(0); // rxdartgo deeper
Know a few core Stream methods (map, where, distinct, asyncMap) and three things only RxDart adds: debounceTime, switchMap and combineLatest2.
Explain that RxDart's operators are extension methods returning plain Streams, and map each rxdart family to the need it fills that core Stream cannot.
Show dependency discipline: keep simple pipelines on core Stream, use RxDart where time, cancellation or combination are needed, and avoid duplicating a state library's own tools.
Set a team rule for when a reactive dependency is justified and where such pipelines live, so the codebase does not grow two ways of expressing the same flow.
## The starting point: what Dart's Stream already does A `Stream<T>` in `dart:async` comes with a solid set of methods for transforming **one** stream: - **Element transforms:** `map`, `where`, `expand`, `cast`. - **Filtering and slicing:** `distinct`, `take`, `skip`, `takeWhile`, `skipWhile`. - **Errors and time limits:** `handleError`, `timeout`. - **Sequential async work:** `asyncMap` (await a `Future` per event, in order) and `asyncExpand` (listen to an inner stream per event, one at a time). Many Flutter features need nothing more: a repository that maps rows to models, a filter over a Firestore-style snapshot stream, a stream of connectivity changes de-duplicated with `distinct`. For those, depending on rxdart only adds a package to keep upgraded. ## What RxDart adds `rxdart` (0.28.0) adds its operators as **extension methods on `Stream`** and static factories on the `Rx` class. They fall into families that core `Stream` genuinely lacks: | Need | Core Stream | rxdart | |---|---|---| | Wait for a pause in input | none | `debounceTime` | | At most one event per window | none | `throttleTime` | | Newest inner stream replaces the running one | none | `switchMap` | | Ignore triggers while one runs | none | `exhaustMap` | | Run inner streams concurrently, capped | none (`asyncExpand` is sequential) | `flatMap(..., maxConcurrent: n)` | | Latest value of each of two sources | none | `Rx.combineLatest2` | | Pair the n-th events of two sources | none | `Rx.zip2` | | Interleave several sources | none | `Rx.merge` | | Sample another source when this one fires | none | `withLatestFrom` | | Emit a value first | none | `startWith` | The package also ships subjects, which are a separate topic. ## Extension methods make mixing free Since rxdart **0.23.0** there is no wrapper type: the old `Observable` class was removed and operators became extensions. That has practical consequences: 1. Any `Stream` — from a `TextEditingController` listener, a plugin, a `StreamController` — gains the operators after `import 'package:rxdart/rxdart.dart';`. 2. Every operator returns a plain `Stream`, so a chain can alternate core and rxdart calls: `map` (core), `debounceTime` (rxdart), `distinct` (core), `switchMap` (rxdart). 3. Code that consumes the result — a `StreamBuilder`, a stream provider, a test with `expectLater` — does not know rxdart was involved. ## A decision rule - If the feature transforms **one stream**, events **independently or strictly in order**, stay on core `Stream`. - If it needs **time** (debounce, throttle), **cancellation of stale work** (switch, exhaust) or **several sources** (combine, zip, merge, sample), rxdart is the concise option. - If the app already uses a state library with its own concurrency tools, check those first; a second mechanism for the same job makes a codebase harder to read. ## A worked contrast A movie list that filters titles containing a query needs `map` and `where` — core only. The same screen's search box that must wait for typing to pause and drop stale responses needs `debounceTime` and `switchMap`, and that is where rxdart pays for itself. ```dart import 'package:rxdart/rxdart.dart'; // Core only: one stream, independent transforms. Stream<List<String>> visibleTitles(Stream<List<String>> all, String genre) => all.map((titles) => titles.where((t) => t.contains(genre)).toList()); // Needs rxdart: time and cancellation. Stream<List<String>> searchTitles( Stream<String> queries, Future<List<String>> Function(String) search, ) => queries .debounceTime(const Duration(milliseconds: 300)) .distinct() .switchMap((q) => Stream.fromFuture(search(q))); ```
- Do you need to wrap a Stream in an RxDart type before calling switchMap on it?No. Since rxdart 0.23.0 the operators are extension methods on `Stream`, so importing `package:rxdart/rxdart.dart` is enough; the old `Observable` wrapper class was removed. The result is a plain `Stream`, usable by any consumer.
- A teammate adds rxdart only to call distinct and asyncMap. What do you say?Both are core `Stream` methods in `dart:async`, so the dependency adds nothing. rxdart is worth adding when the feature needs time-based operators, cancel-or-ignore flattening or multi-source combination, none of which core `Stream` provides.
saying these in an interview costs you the question
- Dart streams cannot do distinct or asyncMap without RxDart.
- You must convert a Stream to an Observable before using RxDart operators.
- Core asyncExpand runs inner streams concurrently like flatMap.
- RxDart operators return a special type that StreamBuilder cannot consume.
- Every Flutter app that uses streams should add RxDart.