Kotlin Flow vs Dart Stream vs Rx — Crossover Interview Advantage
Turn your Flutter experience into an Android interview superpower — async stream models compared
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Kotlin Flow, Dart Stream, and RxJava Observable are all reactive stream implementations that model sequences of asynchronous values. They share core concepts — cold vs hot, operators, backpressure, cancellation — but differ in API style, integration with their language's async model, and ecosystem maturity. Understanding the mapping between them is a significant interview differentiator for multi-platform developers.
Real-world relevance
A developer who built Flutter school apps using Dart StreamControllers for grade update notifications found that transitioning to Android Kotlin was straightforward: StreamController.sink.add() became MutableStateFlow.value =, stream.listen() became collectAsStateWithLifecycle(), and broadcast StreamController became SharedFlow. The conceptual model transferred entirely — only the API names changed.
Key points
- Cold vs hot streams — Cold streams (Kotlin Flow, Dart Stream default) start producing data only when a collector/listener subscribes. Hot streams (StateFlow, SharedFlow, Dart broadcast stream, RxJava Subject) produce data regardless of subscribers — late subscribers miss past emissions.
- Kotlin Flow — cold, suspending — Flow is cold by default. Each collector gets its own independent execution. Built on coroutines — operators are suspend functions. collect{} is a terminal operator that suspends until the flow completes.
- StateFlow — hot, state holder — StateFlow always has a current value (like LiveData). New collectors immediately receive the current state. Perfect for UI state in MVVM: val uiState: StateFlow = _uiState.asStateFlow().
- SharedFlow — hot, event bus — SharedFlow is configurable: replay cache size, buffer size, extra buffer capacity. Used for one-time events (navigation, snackbars) that must not be replayed on screen rotation.
- Dart Stream — single subscription by default — A regular Dart Stream allows only one listener. This mirrors Kotlin Flow's cold single-collection semantics. Adding a second listen() throws a StateError.
- Dart broadcast stream — stream.asBroadcastStream() or StreamController.broadcast() allows multiple listeners — analogous to Kotlin SharedFlow or RxJava PublishSubject. Subscribers joining late miss past events.
- StreamController — Dart's flow builder — StreamController() is Dart's equivalent of Kotlin's MutableSharedFlow or MutableStateFlow. sink.add(value) emits, stream exposes the read-only view — exactly the backing property pattern in Kotlin.
- RxJava Observables — cold by default — Observable.just(), .fromIterable() are cold. BehaviorSubject (has current value, like StateFlow), PublishSubject (no cache, like SharedFlow replay=0), ReplaySubject (full history) are hot Subjects.
- suspend vs async/await — Kotlin suspend functions are the equivalent of Dart async functions returning Future. Both pause execution without blocking the thread. Kotlin uses coroutineScope; Dart uses Zone for structured async context.
- coroutineScope vs Zone — Kotlin's coroutineScope groups coroutines and cancels children on failure — structured concurrency. Dart's Zone is broader: it intercepts async callbacks, error handling, and schedulers for an entire async context tree.
- How Flutter experience helps in interviews — You can explain hot vs cold streams, backpressure concepts, and reactive UI patterns with confidence — these are language-agnostic. Interviewers see a developer who understands async fundamentals, not just Kotlin APIs.
- Operator equivalents across platforms — map, filter, take, debounce, combine, flatMap — these operators exist in Flow, Dart's stream package, and RxJava with nearly identical semantics. Knowing one deeply makes the others learnable in hours.
Code example
// ======= KOTLIN FLOW (Cold) =======
fun gradeUpdates(): Flow<Grade> = flow {
emit(Grade("Math", 95))
delay(1000)
emit(Grade("Science", 88))
}
// Collector 1 gets its own independent execution
viewModelScope.launch {
gradeUpdates().collect { grade -> println(grade) }
}
// ======= KOTLIN StateFlow (Hot — current state) =======
class GradeViewModel : ViewModel() {
private val _uiState = MutableStateFlow(GradeUiState())
val uiState: StateFlow<GradeUiState> = _uiState.asStateFlow()
fun loadGrades() {
viewModelScope.launch {
val grades = repository.fetchGrades()
_uiState.update { it.copy(grades = grades, isLoading = false) }
}
}
}
// Collect in Compose
@Composable
fun GradeScreen(viewModel: GradeViewModel = hiltViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
GradeList(state.grades)
}
// ======= DART STREAM equivalent (single subscription) =======
Stream<Grade> gradeUpdates() async* {
yield Grade('Math', 95);
await Future.delayed(Duration(seconds: 1));
yield Grade('Science', 88);
}
// Listen (single subscriber only)
gradeUpdates().listen((grade) => print(grade));
// ======= DART StreamController (MutableStateFlow equivalent) =======
class GradeNotifier {
final _controller = StreamController<GradeState>.broadcast();
Stream<GradeState> get stream => _controller.stream;
void loadGrades(List<Grade> grades) {
_controller.sink.add(GradeState(grades: grades, isLoading: false));
}
void dispose() => _controller.close();
}
// ======= RxJava BehaviorSubject (StateFlow equivalent) =======
val gradeSubject = BehaviorSubject.createDefault(GradeUiState())
// Emit new state
gradeSubject.onNext(GradeUiState(grades = newGrades))
// Subscribe (gets current value immediately, like StateFlow)
gradeSubject
.observeOn(AndroidSchedulers.mainThread())
.subscribe { state -> renderUi(state) }
// ======= Operator equivalents =======
// Kotlin Flow
gradeFlow.map { it.score }.filter { it >= 60 }.take(5)
// Dart (using stream_transform package)
gradeStream.map((g) => g.score).where((s) => s >= 60).take(5)
// RxJava
gradeObservable.map { it.score }.filter { it >= 60 }.take(5)Line-by-line walkthrough
- 1. flow { emit(...) } — the flow builder is cold: this lambda does not execute until collect{} is called on the returned Flow. Each separate collector triggers a fresh, independent execution of the lambda.
- 2. MutableStateFlow(GradeUiState()) — creates a hot StateFlow with an initial state value; the private _uiState is mutable (backing property pattern), while uiState exposes it as read-only StateFlow.
- 3. _uiState.update { it.copy(...) } — atomically reads and updates StateFlow state using copy(); thread-safe and idiomatic for immutable data classes in Kotlin.
- 4. collectAsStateWithLifecycle() — lifecycle-aware Flow collection in Compose; automatically pauses collection when the composable leaves the STARTED state, preventing wasted work in the background.
- 5. async* / yield in Dart — the async* modifier marks a generator function that produces a Stream; yield emits a single value and suspends; equivalent to Kotlin's flow { emit() } builder.
- 6. StreamController.broadcast() — broadcast() allows multiple listeners; without this, the second listener throws StateError; mirrors Kotlin's MutableSharedFlow with replay=0.
- 7. _controller.sink.add(newState) — Dart's way of emitting to the stream; sink is the write side of the controller, stream is the read side — identical conceptual split to Kotlin's MutableStateFlow vs StateFlow.
- 8. BehaviorSubject.createDefault(initialState) — RxJava's stateful hot Observable; always holds the last emitted value and delivers it immediately to new subscribers, making it semantically equivalent to Kotlin StateFlow.
- 9. observeOn(AndroidSchedulers.mainThread()) — RxJava's equivalent of flowOn(Dispatchers.Main); ensures the subscriber's onNext callback runs on the Android main thread for UI updates.
- 10. gradeFlow.map { it.score }.filter { it >= 60 }.take(5) — demonstrates that operator names (map, filter, take) are shared across Flow, Dart streams, and RxJava; the semantics are identical even if the syntax differs slightly.
Spot the bug
// Android ViewModel using Kotlin Flow
class SchoolViewModel : ViewModel() {
// Bug 1
private val _events = MutableStateFlow<UiEvent?>(null)
val events = _events.asStateFlow()
// Bug 2
private val _grades = MutableSharedFlow<List<Grade>>()
val grades = _grades.asSharedFlow()
fun navigateToDetail(gradeId: String) {
viewModelScope.launch {
_events.value = UiEvent.Navigate(gradeId)
}
}
fun loadGrades() {
viewModelScope.launch {
val result = repository.getGrades()
_grades.emit(result)
}
}
}
// Dart equivalent (for comparison)
class SchoolNotifier {
// Bug 3
final _gradeController = StreamController<List<Grade>>();
Stream<List<Grade>> get gradeStream => _gradeController.stream;
void loadGrades(List<Grade> grades) {
_gradeController.sink.add(grades);
_gradeController.sink.add(grades); // emit twice for cache
}
// Bug 4 — missing dispose
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Kotlin Flow — Official Docs (Kotlin Docs)
- StateFlow and SharedFlow (Android Developers)
- Dart Streams — Official Docs (Dart Docs)
- RxJava vs Kotlin Flow — Comparison (Android Developers Blog)
- Structured Concurrency with coroutineScope (Kotlin Docs)