skip to content

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%

answer

  1. private subject, public ValueStream
  2. unseeded: no fake first fix
  3. listen(subject.add), keep the subscription
  4. sync read for non-widget code
  5. errors replace the replay

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.

solid answer

~40 s

I 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 lines
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();
  }
}

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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.