In RxDart, what do value, valueOrNull and hasValue return on a BehaviorSubject's ValueStream, and when does value throw?
answer
- synchronous peek, no subscription
- unseeded and empty: value throws
- ValueStreamError, an Error subtype
- null seed still counts as a value
- lastEventOrNull since 0.28.0
basics
~20 svalue 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.
solid answer
~40 s`ValueStream` is rxdart's stream with a synchronous memory, and `BehaviorSubject` and its read-only `stream` implement it. `value` returns the latest value and throws `ValueStreamError` if nothing has been emitted yet — which only happens on an unseeded subject before the first `add`. `valueOrNull` returns the value or `null`, and `hasValue` is true once any value exists, including a `null` seed, so it disambiguates nullable types. The value survives `close()` and even a later `addError`: `value` still returns the last data value while `hasError` becomes true. Since 0.28.0, `lastEventOrNull` tells you whether the last event was data or an error. Reading `value` in a widget's build is a snapshot, not a subscription.
code
dart · 18 linesimport 'package:rxdart/rxdart.dart';
typedef Position = ({double lat, double lng});
class LocationCache {
final _subject = BehaviorSubject<Position>();
ValueStream<Position> get positions => _subject.stream;
// Safe before the first fix arrives.
Position? get lastKnown => _subject.valueOrNull;
bool get hasFix => _subject.hasValue;
void update(Position p) => _subject.value = p; // same as add(p)
Future<void> close() => _subject.close();
}go deeper
Remember that value throws on an empty unseeded subject and valueOrNull does not; seeded subjects always have a value.
Explain the full getter table, including the null seed, the value surviving close and addError, and why hasValue exists for nullable element types.
Show how you expose a read-only ValueStream, use lastEventOrNull to tell a stale value from a fresh error, and avoid snapshot reads in build that silently stop updating.
Weigh synchronous reads of shared state against a pure stream API: they are convenient, but they invite logic that depends on timing rather than on events.
## ValueStream: a stream you can also read synchronously `ValueStream<T>` is the rxdart interface for a `Stream<T>` that remembers its last event and lets you read it **without listening**. `BehaviorSubject<T>` implements it, and so does the read-only stream that its `stream` getter returns. That lets a service hand out a `ValueStream<T>`: callers can subscribe for changes *and* peek at the current state, but cannot `add` to it. ## The three value getters | Situation | `value` | `valueOrNull` | `hasValue` | |---|---|---|---| | `BehaviorSubject<int>()`, nothing added | throws `ValueStreamError` | `null` | `false` | | after `add(5)` | `5` | `5` | `true` | | `BehaviorSubject<int?>.seeded(null)` | `null` | `null` | `true` | | after `add(5)`, then `addError(e)` | `5` | `5` | `true` | | after `add(5)`, then `close()` | `5` | `5` | `true` | - `value` returns the last emitted value and **throws** when there is none. The error's own message tells you to check `hasValue` first or to use `valueOrNull`. - `valueOrNull` never throws; it returns the value or `null`. - `hasValue` is true once any value has been emitted — a `null` seed counts. `ValueStreamError` extends `Error`, not `Exception`. In Dart that marks a **programming mistake**: reading state you have not guaranteed exists. The fix is to pick the right getter, not to wrap `value` in `try`/`catch`. ## When value throws — and when it does not 1. An **unseeded** `BehaviorSubject<T>()` with nothing added: `value` throws. 2. A **seeded** subject never throws on `value`, whatever the seed, because the seed is the first value. 3. After **`close()`** the last value stays readable; a closed subject only refuses new events. 4. After **`addError`** the stored value is not cleared, so `value` still returns the last data value. A useful rule: use `value` where the code path guarantees a value (seeded subjects, or after checking `hasValue`), and `valueOrNull` everywhere else. ## Nullable types: why hasValue exists With `BehaviorSubject<Position?>`, `valueOrNull == null` cannot distinguish "no fix yet" from "the fix was explicitly cleared to `null`". `hasValue` separates the two cases. The simpler design is often a **non-nullable** element type with an unseeded subject, so that "no value yet" is expressed by `hasValue` alone. ## Errors and lastEventOrNull The error side mirrors the value side: `error` (throws `ValueStreamError` if there is none), `errorOrNull`, `hasError` and `stackTrace`. - `hasError` means "has emitted at least one error"; a later `add` does **not** reset it. - `lastEventOrNull`, added in **rxdart 0.28.0**, returns the last event as a `StreamNotification` (a `DataNotification` or an `ErrorNotification`), or `null` if nothing was emitted. - The extension getters `isLastEventValue` and `isLastEventError` answer "which came last?" directly — the question `hasValue` plus `hasError` cannot answer. ## Writing through the value setter `BehaviorSubject` also has a `value` **setter**: `subject.value = x` is exactly `subject.add(x)`, so listeners are notified. The setter exists only on the subject; the `ValueStream` handed out by `stream` is read-only. ## Reading value from Flutter code Reading `value` or `valueOrNull` inside `build` takes a **snapshot**; it does not subscribe, so the widget will not rebuild when the subject changes. Listening is still needed for updates. A common bridge is to pass `valueOrNull` as a `StreamBuilder`'s `initialData`, because no stream event can reach the builder before its first build. ```dart import 'package:rxdart/rxdart.dart'; void main() { final unseeded = BehaviorSubject<int>(); print(unseeded.hasValue); // false print(unseeded.valueOrNull); // null final seededNull = BehaviorSubject<String?>.seeded(null); print(seededNull.hasValue); // true unseeded.add(1); unseeded.addError(StateError('GPS lost')); print(unseeded.value); // 1 print(unseeded.hasError); // true print(unseeded.isLastEventError); // true } ```
- Why does a service usually return subject.stream typed as ValueStream rather than the BehaviorSubject itself?`stream` returns a read-only `ValueStream`: callers can listen and read `value`, `valueOrNull` and `hasValue`, but cannot `add`, `addError` or `close`. Returning the subject would let any screen push state or close it, which breaks the single-writer rule a state holder depends on.
- After add(1) and then addError(e) on a BehaviorSubject, what does a new listener receive, and what does value return?The new listener receives the error `e`, because the last event was an error. `value` still returns `1`, because an error does not clear the stored value. `isLastEventError` is true, which is how code can tell the difference in rxdart 0.28.
saying these in an interview costs you the question
- value returns null when the subject has no value yet.
- Wrap value in try/catch instead of checking hasValue or using valueOrNull.
- A BehaviorSubject seeded with null reports hasValue as false.
- Calling addError clears the stored value, so value throws afterwards.
- Reading value inside build makes the widget rebuild when the subject changes.