On a Flutter movie search screen, Rx.combineLatest2 of the query stream and a genre-filter stream shows nothing until a genre is tapped — why, and how do you fix it?
answer
- every source must emit once
- startWith supplies a first value
- withLatestFrom: only one side triggers
- zip pairs, it does not track latest
- then switchMap the combined value
basics
~20 sRx.combineLatest2 emits only after both sources have emitted at least once, and the genre stream emits only on a tap. Give it a starting value with startWith (or a seeded source), then switchMap the combined query and genre to the search call.
solid answer
~50 s`Rx.combineLatest2(a, b, combiner)` stays silent until **every** source has emitted at least once; after that it calls the combiner with the latest value of each whenever either emits. The genre filter only emits when the user taps a chip, so typed queries are stored but never combined. The fix is to give the filter an initial value — `genres.startWith(Genre.all)`, or a seeded source — and do the same for the query if the screen should load before typing. Then `switchMap` the combined `(query, genre)` record to the search so a change to either input cancels the stale request. If only the query should trigger a search, `withLatestFrom` fits better, but it also drops query events until the filter has emitted. `Rx.zip2` is the wrong tool: it pairs the n-th events and would wait for a new genre for every query.
code
dart · 17 linesimport 'package:rxdart/rxdart.dart';
Future<void> main() async {
final queries = Stream.fromIterable(['alien', 'dune']);
final genres = Stream<String>.empty();
// Silent: the genre source never emits.
print(await Rx.combineLatest2(queries, genres, (q, g) => '$q/$g').toList());
// []
final fixed = Rx.combineLatest2(
Stream.fromIterable(['alien', 'dune']),
Stream<String>.empty().startWith('all'),
(q, g) => '$q/$g',
);
print(await fixed.toList()); // [alien/all, dune/all]
}go deeper
Remember that combineLatest2 waits until each source has emitted once, and that startWith gives a stream a first value.
Explain combineLatest2 versus withLatestFrom versus zip2 by what triggers an emission, and why the filter needs a starting value.
Diagnose the silent-screen bug quickly, place debounce on the right input, and end the pipeline with switchMap so changes to either input cancel stale searches.
Decide how screen inputs are modelled — separate streams combined late, or one state object updated by events — and which is easier for the team to test.
## The symptom A movie search screen has a text field and a row of genre chips. The team builds the request stream as `Rx.combineLatest2(queries, genres, (q, g) => (query: q, genre: g))`. Typing does nothing; the first chip tap suddenly shows results for the current text; after that everything works. The bug is not in the search — it is in how `combineLatest2` starts. ## How Rx.combineLatest2 starts and runs `Rx.combineLatest2` (and the `Rx.combineLatest3` ... `Rx.combineLatest9` and `Rx.combineLatestList` forms) subscribes to all sources and remembers each one's latest value. 1. It **does not emit** until every source has emitted at least once. 2. After that, each event from **any** source calls the combiner with the latest value of every source, and the result is emitted. 3. Errors from any source are forwarded; the combined stream completes only when **all** sources have completed. The genre stream is fed by chip taps, so until the first tap it has emitted nothing, and step 1 blocks every query. ## Fix 1: give the late source a first value `startWith` (an rxdart extension) prepends a value to a stream: - `genres.startWith(Genre.all)` makes the filter emit immediately on subscription. - If the screen should show popular movies before any typing, `queries.startWith('')` does the same for the query side. - If the genre lives in a state holder that already has a current value (a seeded subject, for instance), listening to it gives the same effect. ## Fix 2: decide which input triggers the search | Operator | Emits when | Before the other source has emitted | Fits | |---|---|---|---| | `Rx.combineLatest2(q, g, f)` | either source emits (after both have) | silent | query **or** genre change re-runs the search | | `q.withLatestFrom(g, f)` | only the query emits | query events are **dropped** | genre is context, not a trigger | | `Rx.zip2(q, g, f)` | both have a new n-th event | silent | lockstep pairs — not this screen | | `Rx.merge([a, b])` | any source emits, values passed through unchanged | passes through | several triggers of the same type | For a filter chip the user expects the list to refresh when the genre changes, so `combineLatest2` is the right operator — once both sides have a starting value. `withLatestFrom` has the same startup trap in a different form: it silently drops query events until the genre has emitted, so it needs `startWith` too. ## Fix 3: flatten the combined request with switchMap `combineLatest2` emits a new `(query, genre)` pair on every change of either input. Feed it into `switchMap` so a change to either one cancels the subscription to the stale search; apply `debounceTime` to the query stream before combining, not after, so chip taps take effect immediately. ## Merge for extra triggers A "retry" button can be folded in with `Rx.merge`: map each tap to the latest request and merge that stream with the combined requests before `switchMap`, so both paths share one flattening policy. ```dart import 'package:rxdart/rxdart.dart'; enum Genre { all, drama, scifi } typedef MovieQuery = ({String text, Genre genre}); Stream<List<String>> results({ required Stream<String> typed, required Stream<Genre> genreTaps, required Future<List<String>> Function(MovieQuery) search, }) { final queries = typed .debounceTime(const Duration(milliseconds: 300)) .startWith(''); final genres = genreTaps.startWith(Genre.all); return Rx.combineLatest2<String, Genre, MovieQuery>( queries, genres, (text, genre) => (text: text, genre: genre), ).distinct().switchMap((q) => Stream.fromFuture(search(q))); } ``` Records compare by value, so the core `distinct()` after `combineLatest2` suppresses a pair identical to the previous one — for example tapping the chip that is already selected.
- Why debounce the query stream before combineLatest2 rather than debouncing the combined stream?Debouncing the combined stream would delay chip taps as well, making the filter feel sluggish, and a keystroke followed quickly by a tap would be merged into one emission. Debouncing only the text stream keeps typing rate-limited while genre changes take effect immediately.
- With withLatestFrom instead of combineLatest2, what does the user see if they type before tapping a genre?Nothing for those queries: `withLatestFrom` emits only when the source (the query) emits and the other stream already has a value; earlier query events are dropped, not buffered. After the first genre tap, only the next query triggers a search. `startWith` on the genre stream fixes it.
saying these in an interview costs you the question
- combineLatest2 emits as soon as any one source emits.
- withLatestFrom buffers source events until the other stream emits.
- zip2 is the same as combineLatest2 but faster.
- The combined stream completes as soon as one source completes.
- Debounce the combined stream so chip taps are rate-limited too.