How would you build a Flutter current-location service on an RxDart BehaviorSubject so screens opened later see the last position?
answer
- private subject, public ValueStream
- unseeded: no fake first fix
- listen(subject.add), keep the subscription
- sync read for non-widget code
- errors replace the replay
basics
~10 sKeep 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.
solid answer
~40 sI keep a private `BehaviorSubject<Position>` in the service and feed it with `_sub = platformStream.listen(_subject.add, onError: _subject.addError)`, holding on to the subscription. Publicly I expose `ValueStream<Position> get positions => _subject.stream` and `Position? get lastKnown => _subject.valueOrNull`, so screens can listen and non-widget code can read synchronously, but nobody else can write. I leave it **unseeded** rather than inventing a (0, 0) fix; `hasValue` means we have a real position. Because the subject is broadcast and replays its latest value, a map screen pushed later gets the last fix immediately. Errors need a decision: an error becomes what late listeners receive, so permission failures often go to a separate stream. `dispose` cancels the subscription, then closes the subject.
code
dart · 22 linesimport 'dart:async';
import 'package:rxdart/rxdart.dart';
typedef Position = ({double lat, double lng});
class LocationService {
LocationService(Stream<Position> platformFixes) {
_sub = platformFixes.listen(_subject.add, onError: _subject.addError);
}
final _subject = BehaviorSubject<Position>();
late final StreamSubscription<Position> _sub;
ValueStream<Position> get positions => _subject.stream;
Position? get lastKnown => _subject.valueOrNull;
Future<void> dispose() async {
await _sub.cancel();
await _subject.close();
}
}go deeper
Know that the service keeps the subject private and hands out its stream, and that a BehaviorSubject replays the last fix to a screen opened later.
Explain seeded versus unseeded for data with no honest initial value, the read-only ValueStream, and why the upstream subscription must be kept for cancelling.
Show the traps you have met: addStream blocking close, a replayed error hiding the last good fix, and the first-frame spinner avoided by seeding the builder from valueOrNull.
Argue when a subject-backed service is acceptable next to a team's chosen state library, and how you would enforce single-writer and disposal rules in review.
## The job: current state that late screens can see A **current-location service** has one piece of state — the latest position fix — and several consumers: a map screen, a "nearby" list, a distance badge, maybe a screen pushed minutes later. A `BehaviorSubject` from `rxdart` fits because it is a **broadcast** controller (many listeners) that **replays its latest value** to every new listener and exposes that value synchronously through the `ValueStream` getters. ## The shape of the service 1. **A private subject.** `final _subject = BehaviorSubject<Position>();` — only the service writes. 2. **A read-only public stream.** `ValueStream<Position> get positions => _subject.stream;` — `stream` returns a read-only `ValueStream`, so callers can `listen` and read `value`/`valueOrNull`/`hasValue`, but cannot `add` or `close`. 3. **A synchronous getter** for code that needs the last fix now, such as building a request: `Position? get lastKnown => _subject.valueOrNull;`. 4. **An upstream subscription you own.** `_sub = source.listen(_subject.add, onError: _subject.addError);` keeps a `StreamSubscription` you can cancel later. 5. **A `dispose` method** that cancels `_sub` and then calls `_subject.close()`. ## Seeded or unseeded? | Choice | First listener receives | `hasValue` before the first fix | Risk | |---|---|---|---| | `BehaviorSubject<Position>()` | nothing until the first fix | `false` | screens must render a "locating" state | | `BehaviorSubject.seeded((lat: 0, lng: 0))` | the fake fix | `true` | the map jumps to the Gulf of Guinea | | `BehaviorSubject<Position?>.seeded(null)` | `null` | `true` | every consumer must handle `null` | Seed only with a **real** initial state. For location there is none, so the unseeded, non-nullable version keeps "no fix yet" honest: `hasValue` is false and `valueOrNull` is null. ## Feeding it: listen, not addStream `Subject.addStream(source)` looks tidier, but while it is active the subject rejects `add`, `addError`, `close` and a second `addStream` with a `StateError`. A platform location stream never completes, so a service that piped it with `addStream` could not close its subject. Listening and keeping the `StreamSubscription` gives you a handle to cancel. ## Errors change what late listeners get If the source reports an error through `addError`, the subject replays **that error**, not the last position, to every new listener until the next fix arrives — while `value` still returns the old position. Decide deliberately: - Forward errors when consumers genuinely need to show a failure state. - Otherwise put permission or service failures on a separate status stream (often a `PublishSubject` for one-off events), so a newly opened map still sees the last known fix. - In rxdart 0.28, `isLastEventError` tells non-widget code which case it is in. ## Consuming it in Flutter Listen to `positions` from the widget layer through whatever the app uses — a `StreamBuilder`, a stream provider, a bloc. Because no stream event can reach a `StreamBuilder` before its first build, pass `positions.valueOrNull` as its `initialData` so a screen opened later draws the known fix in its first frame instead of a spinner. ## When not to use a raw subject A subject-backed service is a sound, library-light state holder, and it composes directly with rxdart operators. Its weaknesses are the ones interviewers probe: - Nothing enforces a single writer except your own discipline about keeping the subject private. - Lifecycle is manual: somebody must call `dispose`. - On a team that has standardised on BLoC or Riverpod, the same job is usually a cubit or a stream provider, and a raw subject is an inconsistency. ```dart import 'dart:async'; import 'package:rxdart/rxdart.dart'; typedef Position = ({double lat, double lng}); class LocationService { LocationService(Stream<Position> platformFixes) { _sub = platformFixes.listen(_subject.add, onError: _subject.addError); } final _subject = BehaviorSubject<Position>(); late final StreamSubscription<Position> _sub; ValueStream<Position> get positions => _subject.stream; Position? get lastKnown => _subject.valueOrNull; Future<void> dispose() async { await _sub.cancel(); await _subject.close(); } } ```
- Why not expose the BehaviorSubject itself as a public field?The subject is also a sink: any caller could `add` a position, `addError`, or `close` it and break every other screen. Exposing `_subject.stream` hands out a read-only `ValueStream` with the same replay and synchronous getters, and keeps the service the only writer.
- Why is piping the platform stream with subject.addStream a trap here?While an `addStream` is active the subject throws `StateError` on `add`, `addError`, `close` and another `addStream`. A location stream never finishes, so `dispose` could not close the subject. `listen` with a stored `StreamSubscription` gives a handle to cancel first and close after.
saying these in an interview costs you the question
- Seed the location subject with (0, 0) so listeners always get something.
- Expose the BehaviorSubject publicly so screens can push corrections.
- Use subject.addStream for the platform feed; it closes itself when the screen closes.
- A late listener always gets the last position, even after the source reported an error.
- A PublishSubject works the same, since screens listen before fixes arrive.