BehaviorSubject (RxDart)
4 min read
State Management
StreamController<T> | BehaviorSubject<T> | PublishSubject<T> | ReplaySubject<T> | |
|---|---|---|---|---|
| Initial / current value | ❌ | ✅ (seeded) | ❌ | Replays N values |
| Late subscriber sees… | Nothing | Latest value | Future emissions only | Past N + future |
Synchronous .value getter | ❌ | ✅ | ❌ | ❌ |
| Single or broadcast | Either | Always broadcast | Always broadcast | Always broadcast |
Mental model: BehaviorSubject is a hot variable + stream rolled into one.
Code in action — search with debounce and switchMap
import 'package:rxdart/rxdart.dart';
class SearchBloc {
SearchBloc(this._api) {
results = _query
.debounceTime(const Duration(milliseconds: 300)) // wait for typing pause
.distinct() // ignore repeats
.where((q) => q.length >= 2)
.switchMap((q) => _api.search(q) // cancel in-flight on new query
.asStream()
.startWith(<Result>[]) // show empty while loading
.onErrorReturn(<Result>[]))
.share(); // one upstream sub, many listeners
}
final ApiClient _api;
final _query = BehaviorSubject<String>.seeded('');
late final Stream<List<Result>> results;
String get current => _query.value; // read synchronously
void search(String q) => _query.add(q);
void dispose() => _query.close();
}
// Combine multiple streams of state into one
final state = Rx.combineLatest3<User?, List<Notif>, Settings, DashboardState>(
_user, _notifs, _settings,
(u, n, s) => DashboardState(u, n, s),
);
When to use which stream type
| Goal | Use |
|---|---|
| Reactive state with a "current value" | BehaviorSubject |
| Event stream — past events don't matter | PublishSubject |
| Need to replay last N events to late subscribers | ReplaySubject |
| Simple async, no operators | Plain Stream / StreamController |
| Combine / debounce / switchMap | RxDart |
| Anything in pure Flutter without an extra dependency | Plain streams + manual logic, or Riverpod's stream providers |
Common mistakes to avoid
// ❌ Forgetting to close — leaks
final _subject = BehaviorSubject<String>();
// dispose() override missing → controller lives forever
// ❌ Using mergeMap when switchMap is correct for cancellation
.mergeMap((q) => api.search(q)) // results come back out of order
// ✅ switchMap cancels stale requests when a new query arrives
// ❌ Skipping .share() when many widgets listen → each gets its own upstream
final r = results.where(...); // every listener triggers the whole chain
// ✅ results = stream.share() or .shareReplay()
// ❌ Reading .value before any event was added
final s = BehaviorSubject<String>();
print(s.value); // throws — no value yet
// ✅ BehaviorSubject.seeded('initial')
// ❌ Adopting RxDart for simple state where setState/Provider would do
// RxDart shines for complex stream pipelines. For "is a dialog open" — overkill.
Interview follow-ups
-
What's the difference between
BehaviorSubjectandValueNotifier? Both expose a current value + change notifications.ValueNotifierships with Flutter, is synchronous, and integrates withAnimatedBuilder/ValueListenableBuilder.BehaviorSubjectis a stream — you get operator composition (debounce, combineLatest, switchMap) and can pass it across isolates of code that already speak streams. Use ValueNotifier for simple UI bindings, BehaviorSubject when you actually need stream operators. -
When does
switchMapmatter vsflatMap/mergeMap?switchMapcancels the previous inner stream when a new value arrives — essential for search, autocomplete, and rapid input where stale results would race.mergeMap(flatMap) runs all inner streams concurrently — fine for fire-and-forget side effects, dangerous for search. -
What does
.share()actually do? It multicasts a stream — one subscription upstream, fanned out to N downstream listeners. Without it, each subscriber starts its own upstream pipeline..shareReplay(maxSize: 1)is.share()+ buffer of the last value, so late subscribers get the most recent emission. -
When would you NOT use RxDart in a Flutter app? When you'd add it just for
BehaviorSubject(replace withValueNotifier), when your state manager already covers stream-style flows (Riverpod'sStreamProvider, BLoC's built-in stream handling), or when you're optimising for bundle size and don't actually use the operators. RxDart is a hammer — make sure the nail is there.
How helpful was this content?
Please sign in to rate this article.