In RxDart, what does a new listener receive from a BehaviorSubject, a PublishSubject and a ReplaySubject, and which suits UI state?
answer
- memory at subscribe time
- latest one vs none vs a buffer
- all three wrap a broadcast controller
- seeded gives a value before any add
- maxSize null means unbounded
basics
~20 sA 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 sAll 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 linesimport '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
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.
Explain seeded versus unseeded, that all three are broadcast, that errors can be replayed, and that ReplaySubject's maxSize defaults to unbounded.
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.
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.