Lesson 48 of 83 intermediate

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

Think of async streams like water delivery systems. Kotlin Flow is a pipe that only flows when someone connects a tap (cold, on demand). Dart Stream is similar but has two modes — a private pipe (single subscriber) and a municipal main (broadcast, many listeners). RxJava Observable is a vintage system with 200 types of valves and adaptors — incredibly powerful but you need a plumber's license to operate it safely.

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

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. 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. 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. 3. _uiState.update { it.copy(...) } — atomically reads and updates StateFlow state using copy(); thread-safe and idiomatic for immutable data classes in Kotlin.
  4. 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. 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. 6. StreamController.broadcast() — broadcast() allows multiple listeners; without this, the second listener throws StateError; mirrors Kotlin's MutableSharedFlow with replay=0.
  7. 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. 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. 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. 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?
Check which Flow type should be used for one-time navigation events vs persistent state, which type should hold grade lists, the Dart StreamController broadcast issue, and resource cleanup.
Show answer
Bug 1: MutableStateFlow<UiEvent?>(null) for navigation events — StateFlow is wrong for one-time events like navigation. StateFlow replays its current value to every new collector. When the screen rotates and the composable re-subscribes, it will immediately re-receive the Navigate event, causing a double navigation. Fix: use MutableSharedFlow<UiEvent>(extraBufferCapacity = 1) with replay = 0 (the default). This emits the event once and does not replay it to late collectors. Bug 2: MutableSharedFlow<List<Grade>>() for the grade list — SharedFlow is wrong for UI state. SharedFlow does not hold a current value, so if the Compose UI re-subscribes after rotation, it receives nothing until the next emission and shows a blank/loading screen unnecessarily. Fix: use MutableStateFlow<List<Grade>>(emptyList()) which always holds the current grade list and delivers it immediately to new collectors. The bugs in 1 and 2 are inverted — the developer used StateFlow where SharedFlow was needed and SharedFlow where StateFlow was needed. Bug 3: StreamController<List<Grade>>() without .broadcast() — a regular StreamController allows only one listener. In Flutter with Provider/Riverpod, multiple widgets may subscribe to gradeStream. The second listener will throw a StateError ('Stream has already been listened to'). Fix: StreamController<List<Grade>>.broadcast(). Note: for state that should be replayed to new listeners (like a grade list), Dart's RxDart BehaviorSubject (which replays the last value) is the correct equivalent of MutableStateFlow. Bug 4: No dispose() method — the StreamController holds resources. If SchoolNotifier is used in a StatefulWidget or a Provider, the controller must be closed when it is no longer needed: void dispose() { _gradeController.close(); }. Without this, the Dart VM reports 'StreamSink was not closed' and may leak memory in long-running apps.

Explain like I'm 5

Imagine a TV channel (hot stream) — it broadcasts shows whether you are watching or not. If you turn on late, you miss the beginning. Now imagine a DVD player (cold stream) — it starts playing from the beginning for each person who presses play, just for them. Kotlin Flow is the DVD player by default. StateFlow is like a digital TV guide that always shows the current program — even if you check it late, you see what is on NOW. Dart Stream is also a DVD player but only one person can watch each DVD (no sharing). Broadcast Stream is the TV channel version.

Fun fact

The reactive streams specification (reactive-streams.org) that underpins Kotlin Flow and RxJava was co-authored by engineers from Netflix, Pivotal, Red Hat, Twitter, and Typesafe in 2014. Dart's Stream predates this spec (Dart 1.0 in 2013) and follows similar principles but was designed independently. All three converged on the same conceptual model through parallel evolution.

Hands-on challenge

You are interviewing for a senior Android role and the interviewer knows you have Flutter experience. They ask: 'How does Dart Stream compare to Kotlin Flow? Give me a concrete example where you would use SharedFlow instead of StateFlow, and its Dart broadcast StreamController equivalent.' Write a full answer with code examples for both platforms, explain the hot vs cold distinction, and describe one scenario (e.g., one-time navigation events) where SharedFlow is the right choice over StateFlow.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery