skip to content

RxDart

RxDart layers ReactiveX subjects and operators such as switchMap, debounceTime and combineLatest onto Dart's own Streams. Interviewers ask when the core Stream API stops being enough.

on this pageshow

explore

questions

9

In RxDart, what does a new listener receive from a BehaviorSubject, a PublishSubject and a ReplaySubject, and which suits UI state?

level: juniorimportance: must knowfreq 55%

answer

  1. memory at subscribe time
  2. latest one vs none vs a buffer
  3. all three wrap a broadcast controller
  4. seeded gives a value before any add
  5. maxSize null means unbounded

basics

~20 s

A late BehaviorSubject listener first gets the latest value (or error), a PublishSubject listener gets only later events, and a ReplaySubject listener gets the buffered history, capped by maxSize. BehaviorSubject suits UI state: late listeners see the current value.

solid answer

~40 s

All three rxdart subjects are broadcast `StreamController`s that are also streams; they differ in what a listener that arrives late receives. `BehaviorSubject` caches one item and replays the latest value (or the latest error, if that came last) before live events, and `BehaviorSubject.seeded(x)` provides a value before anything is added. `PublishSubject` replays nothing, so a late listener only sees future events. `ReplaySubject` replays a queue of past events, which is unbounded unless you pass `maxSize`. For UI state I pick `BehaviorSubject`: a widget that subscribes after navigation gets the current state at once, and code can read it synchronously through `value`/`valueOrNull`. `PublishSubject` is for one-off events such as a snackbar trigger.

code

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

typedef Position = ({double lat, double lng});

Future<void> main() async {
  final location = BehaviorSubject<Position>();
  final permissionDenied = PublishSubject<void>();

  location.add((lat: 52.52, lng: 13.40));
  location.add((lat: 52.53, lng: 13.41));
  permissionDenied.add(null); // dropped: nobody is listening yet

  // A screen that opens later still sees the last fix.
  location.listen((p) => print('map centred on $p'));
  permissionDenied.listen((_) => print('show permission banner'));

  await Future<void>.delayed(Duration.zero);
  await location.close();
  await permissionDenied.close();
}

go deeper

for a junior

Recall one line per subject: Behavior replays the latest, Publish replays nothing, Replay replays a buffer. Then name BehaviorSubject for screen state and say why.

for a middle

Explain seeded versus unseeded, that all three are broadcast, that errors can be replayed, and that ReplaySubject's maxSize defaults to unbounded.

for a senior

Show judgement about event versus state: one-off navigation or snackbar events belong on a PublishSubject so they are not replayed, and an unbounded ReplaySubject on a long-lived source is a memory risk.

for a principal

Discuss when a raw subject is still the right tool in a codebase that has adopted BLoC or Riverpod, and how to keep subjects private behind read-only streams across team boundaries.

## What a subject is in RxDart A **subject** in the `rxdart` package (0.28.0) is a `StreamController` that is also a `Stream`: you push events with `add`, `addError` and `close`, and anyone can `listen` to it directly or to its `stream` getter. All three built-in subjects — `BehaviorSubject`, `PublishSubject` and `ReplaySubject` — wrap a **broadcast** controller from `dart:async`, so several listeners can subscribe at the same time. What separates them is **memory**: what a listener that subscribes *after* some events were added receives first. - Each factory takes optional `onListen` and `onCancel` callbacks and a `sync` flag that defaults to `false`. - Each is both the sink you write to and the source you listen to, which is why a service normally keeps the subject private and exposes only its stream. ## The three subjects side by side | Subject | What a new listener receives first | Synchronous read | Typical use | |---|---|---|---| | `BehaviorSubject` | the latest value, or the latest error if that came last | `value`, `valueOrNull`, `hasValue` | current state | | `PublishSubject` | nothing — only events added after it subscribed | none | one-off events | | `ReplaySubject` | the buffered events, oldest first, up to `maxSize` | `values`, `errors` | a short history | ## BehaviorSubject: the latest value, optionally seeded `BehaviorSubject` keeps one slot. Every `add` overwrites it; a new listener receives the cached item first and then every later event. `BehaviorSubject.seeded(x)` fills the slot at construction, so the subject has a current value before anything is added. Because it implements **`ValueStream`**, the slot can also be read synchronously with `value`, `valueOrNull` and `hasValue`. - If the last event added was an **error**, a new listener receives that error instead of a value. - The next `add` restores normal replay of the latest value. ## PublishSubject: only what happens from now on `PublishSubject` is a broadcast controller that is also a stream, with no memory at all. Events added while nobody listens are dropped, and a listener that arrives late has missed them for good. That is the right shape for **events that should fire once**: show a snackbar, navigate to a result screen, report that location permission was denied. Replaying those to a screen that subscribes later would repeat an action the user has already seen. ## ReplaySubject and maxSize `ReplaySubject` keeps a queue of past events — data *and* errors — and replays them in order to each new listener before live events. 1. `maxSize` is **nullable and null by default, which means unbounded**: a replay subject fed by a long-lived source keeps growing for as long as it lives. 2. With `maxSize: 2`, adding 1, 2 and 3 drops 1, and each new listener receives 2 then 3. 3. Errors occupy slots in the same queue, so they count towards `maxSize` too. 4. The buffered data is readable through `values` and the buffered errors through `errors`. ## Which one for UI state For state a screen renders — the device's current position, a cart total, an active filter — interviewers expect **`BehaviorSubject`**: - A widget that subscribes late (after a route push, or after a rebuild swapped its stream) still gets the current value without waiting for the next change. - Code outside the widget tree can read the current state synchronously. - It stores one item, so its memory does not grow with the number of updates. `PublishSubject` would leave a late screen blank until the next update arrives, and `ReplaySubject` without `maxSize` would replay — and retain — every past state. `ReplaySubject(maxSize: 1)` replays like a behaviour subject but offers `values` (a list) instead of the single-value `ValueStream` getters, and cannot be seeded. ```dart import 'package:rxdart/rxdart.dart'; Future<void> main() async { final behavior = BehaviorSubject<int>(); final publish = PublishSubject<int>(); final replay = ReplaySubject<int>(maxSize: 2); for (final subject in [behavior, publish, replay]) { subject..add(1)..add(2)..add(3); } behavior.listen((v) => print('behavior $v')); // behavior 3 publish.listen((v) => print('publish $v')); // nothing replay.listen((v) => print('replay $v')); // replay 2, replay 3 await Future<void>.delayed(Duration.zero); await Future.wait([behavior.close(), publish.close(), replay.close()]); } ``` The one-line version for an interview: **Behavior remembers the last, Publish remembers nothing, Replay remembers a buffer** — and UI state wants the last.

  • What does BehaviorSubject.seeded add over the plain BehaviorSubject constructor?
    `BehaviorSubject.seeded(x)` stores `x` as the current value at construction, so `hasValue` is already true, `value` does not throw, and the first listener receives `x` even though nothing was added. Use it when a real initial state exists, such as an empty list or a zero total; do not invent a fake seed for data that genuinely has no value yet.
  • Does a BehaviorSubject replay errors to new listeners?
    Yes, when the last event was an error: a new listener receives that error first. As soon as a later `add` arrives, replay switches back to the latest value. A `ReplaySubject` also replays errors, in their position in its buffer.

A BehaviorSubject is a departures board: whoever walks in sees the current status at once. A PublishSubject is a station announcement: miss it and it is gone. A ReplaySubject is a recording of the last few announcements, played to each newcomer.

saying these in an interview costs you the question

  • PublishSubject replays the last value to listeners that subscribe late.
  • BehaviorSubject is single-subscription, so only one widget can listen to it.
  • ReplaySubject keeps only a small buffer by default.
  • BehaviorSubject never replays an error, only data values.
  • A plain broadcast StreamController already replays its latest value.
open as a page

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%

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.

open as a page

In a Flutter app, when do Dart's core Stream methods suffice, and which RxDart operators have no core Stream equivalent?

level: juniorimportance: should knowfreq 36%

basics

~20 s

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

open as a page

In RxDart, what do throttleTime's leading and trailing defaults do, and why would a throttled movie search miss the final query?

level: middleimportance: should knowfreq 34%

basics

~20 s

throttleTime defaults to leading: true and trailing: false: it emits the first event, then drops everything else for the duration. Typing "dune" quickly searches for "d" and never for "dune"; search needs debounceTime or trailing: true.

open as a page

How would you build a Flutter current-location service on an RxDart BehaviorSubject so screens opened later see the last position?

level: middleimportance: should knowfreq 32%

basics

~10 s

Keep a private unseeded BehaviorSubject fed from the platform location stream, expose its read-only ValueStream plus valueOrNull, and close it with the upstream subscription. Screens opened later receive the last position on subscribe.

open as a page

In RxDart, what do value, valueOrNull and hasValue return on a BehaviorSubject's ValueStream, and when does value throw?

level: middleimportance: should knowfreq 38%

basics

~20 s

value returns the last emitted value and throws ValueStreamError when there is none; valueOrNull returns it or null; hasValue says whether any value, including a null seed, was emitted. Only an unseeded, never-added BehaviorSubject makes value throw.

open as a page

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?

level: seniorimportance: should knowfreq 38%

basics

~20 s

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

open as a page

In a Flutter app, what goes wrong when an RxDart subject owned by a screen's controller is never closed, and what does close() change?

level: seniorimportance: should knowfreq 34%

basics

~20 s

An unclosed subject keeps its listeners, their captured objects and any upstream feed alive while something long-lived references it, and listeners waiting for done never finish. close() sends done to every listener and makes later add or addError throw StateError.

open as a page