Flow questions are becoming a standard part of Android interviews, especially at mid and senior levels. Interviewers want to know that you understand cold vs hot streams, can pick the right operator for the job, know how to collect safely with lifecycle awareness, and can explain when to use StateFlow vs SharedFlow vs LiveData. This guide covers the Flow interview questions you’re most likely to face.
Flow Fundamentals
1. What is a Flow? How is it different from LiveData?
// Flow is a cold, asynchronous stream that emits values sequentially
// It's part of Kotlin coroutines — pure Kotlin, no Android dependency
val flow = flow {
emit(1)
delay(1000)
emit(2)
}
// LiveData vs Flow:
// LiveData: Android-only, lifecycle-aware built-in, limited operators (map, switchMap)
// Flow: pure Kotlin, 50+ operators, works in KMP, needs repeatOnLifecycle for safety
// Flow advantages: richer operators, threading flexibility (flowOn),
// proper one-time events (SharedFlow), testable with Turbine
// LiveData advantage: simpler lifecycle-aware observation (one line)
2. What does “cold” mean in the context of Flow?
// COLD = code inside flow {} doesn't run until someone calls collect
// Each collector gets its own independent execution
val coldFlow = flow {
println("Flow started") // runs fresh for EACH collector
emit(System.currentTimeMillis())
}
// Collector 1 — triggers "Flow started", gets timestamp
coldFlow.collect { println("A: $it") }
// Collector 2 — triggers "Flow started" AGAIN, gets a different timestamp
coldFlow.collect { println("B: $it") }
// HOT streams (StateFlow, SharedFlow) exist independently of collectors
// They emit values regardless of whether anyone is listening
3. What are the different ways to create a Flow?
// flow {} — most common, for async operations
val f1 = flow { emit(1); delay(100); emit(2) }
// flowOf() — from fixed values
val f2 = flowOf(1, 2, 3)
// .asFlow() — from collections
val f3 = listOf(1, 2, 3).asFlow()
// callbackFlow {} — bridge callback-based APIs
val f4 = callbackFlow {
val callback = object : Listener {
override fun onData(data: Data) { trySend(data) }
}
register(callback)
awaitClose { unregister(callback) }
}
// channelFlow {} — when you need to emit from different coroutines
val f5 = channelFlow {
launch { send(fetchA()) }
launch { send(fetchB()) }
}
4. What is the difference between collect, first, toList, and reduce?
val numbers = flowOf(1, 2, 3, 4, 5)
// collect — process each value, suspends until Flow completes
numbers.collect { println(it) } // 1, 2, 3, 4, 5
// first() — get only the first value, then cancel
val first = numbers.first() // 1
// toList() — collect all values into a List
val list = numbers.toList() // [1, 2, 3, 4, 5]
// reduce — accumulate values
val sum = numbers.reduce { acc, value -> acc + value } // 15
// All of these are TERMINAL operators — they trigger collection
Operators
5. What is the difference between map, transform, and flatMapLatest?
// map — 1-to-1 transformation
val names = userFlow.map { it.name }
// transform — flexible, emit 0 or more values per input
val detailed = userFlow.transform { user ->
emit(UiState.Loading)
val details = fetchDetails(user.id)
emit(UiState.Success(details))
}
// flatMapLatest — cancel previous inner Flow when new value arrives
val results = queryFlow.flatMapLatest { query ->
repository.search(query) // previous search CANCELLED on new query
}
// flatMapLatest is THE operator for search, live filters, user selection
// flatMapConcat for sequential processing (pagination)
// flatMapMerge for parallel processing (batch downloads)
6. What is the difference between combine and zip?
// combine — emits whenever EITHER Flow emits, uses latest from both
combine(queryFlow, filterFlow) { query, filter ->
repository.search(query, filter)
}
// Query changes → re-emit. Filter changes → re-emit.
// Always uses the LATEST value from both.
// zip — pairs values 1-to-1, waits for both
names.zip(ages) { name, age -> "$name: $age" }
// Waits for one value from EACH flow before emitting
// Completes when EITHER flow completes
// combine: "always use the latest" (UI state from multiple sources)
// zip: "pair them up" (matching request-response pairs)
7. What is debounce? When do you use it?
// debounce waits for a PAUSE in emissions before passing the value through
val searchResults = queryFlow
.debounce(300) // wait 300ms after last keystroke
.distinctUntilChanged() // skip if query hasn't changed
.filter { it.isNotBlank() } // skip empty
.flatMapLatest { query ->
repository.search(query)
}
// User types: "k" "ko" "kot" "kotl" "kotlin"
// Without debounce: 5 API calls
// With debounce(300): 1 API call ("kotlin" — after 300ms pause)
// Use debounce for: search input, text field validation, sensor data
8. What is the difference between buffer, conflate, and collectLatest?
// When collector is SLOWER than emitter:
// buffer — emitter and collector run concurrently, values queue up
flow.buffer().collect { slowProcess(it) }
// Gets ALL values, but emitter doesn't wait for collector
// conflate — drop intermediate values, collector always gets latest
flow.conflate().collect { slowProcess(it) }
// Skips values that arrive while collector is busy
// collectLatest — cancel previous collection when new value arrives
flow.collectLatest { value ->
val rendered = heavyRender(value) // cancelled if new value arrives
updateUi(rendered)
}
// buffer: "I need every value, just let emitter run ahead"
// conflate: "only latest matters, skip intermediate"
// collectLatest: "restart processing with newest value"
Context & Threading
9. What is flowOn? Why can’t you use withContext inside flow {}?
// flowOn changes the UPSTREAM dispatcher
flow {
emit(heavyComputation()) // runs on Default (because of flowOn)
}
.flowOn(Dispatchers.Default) // affects everything ABOVE it
.collect { updateUi(it) } // runs on collector's context (Main)
// ❌ withContext inside flow {} throws IllegalStateException
flow {
withContext(Dispatchers.IO) {
emit(fetchData()) // emission from different context = CRASH
}
}
// Why? Flow guarantees sequential, context-preserving emissions
// withContext would break this guarantee by switching context mid-emission
// flowOn is the correct, safe way to change emission context
10. What is the difference between intermediate and terminal operators?
// Intermediate operators return a new Flow — they DON'T trigger collection
val transformed = flow
.filter { it > 0 } // intermediate — returns Flow
.map { it * 2 } // intermediate — returns Flow
.debounce(300) // intermediate — returns Flow
// Nothing has executed yet! No values have been processed.
// Terminal operators TRIGGER collection — they suspend until Flow completes
transformed.collect { println(it) } // terminal — starts the chain
val list = transformed.toList() // terminal
val first = transformed.first() // terminal
val count = transformed.count() // terminal
// Think of it like a pipeline:
// Intermediate operators BUILD the pipeline
// Terminal operators TURN ON the tap
StateFlow & SharedFlow
11. What is the difference between StateFlow and SharedFlow?
// StateFlow — always has a current value, replays latest, skips duplicates
// Use for: UI state
val _uiState = MutableStateFlow(UiState.Loading)
// .value always available, conflated, distinctUntilChanged built-in
// SharedFlow — no current value, configurable replay, delivers all emissions
// Use for: one-time events
val _events = MutableSharedFlow<UiEvent>()
// No .value, replay = 0 by default, delivers duplicates
// StateFlow is like a variable that you can observe
// SharedFlow is like an event bus
12. Why shouldn’t you use StateFlow for one-time events?
// ❌ StateFlow replays the last value to new collectors
val _event = MutableStateFlow<String?>(null)
fun save() { _event.value = "Saved!" }
// Problem: on screen rotation, new collector sees "Saved!" again
// Snackbar shows twice!
// ✅ SharedFlow with replay = 0 — consumed once
val _event = MutableSharedFlow<String>()
fun save() {
viewModelScope.launch { _event.emit("Saved!") }
}
// New collectors after rotation don't see the old event
13. What is stateIn? What does WhileSubscribed(5000) mean?
// stateIn converts a cold Flow to a hot StateFlow
val articles: StateFlow<List<Article>> = repository.getArticlesFlow()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
// SharingStarted options:
// Eagerly — starts immediately, never stops
// Lazily — starts on first collector, never stops
// WhileSubscribed(5000) — starts on first collector,
// stops 5 SECONDS after last collector unsubscribes
// Why 5000ms?
// Screen rotation takes ~1-2 seconds
// 5s timeout survives rotation without restarting the upstream Flow
// But stops the Flow when user truly leaves the screen (saves resources)
// WhileSubscribed(5000) is the recommended default for Android UI
14. How do you collect multiple Flows in a Fragment?
// ❌ WRONG — second collect is never reached (first suspends forever)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { /* ... */ } // suspends forever
viewModel.events.collect { /* ... */ } // never reached!
}
}
// ✅ CORRECT — launch each collection in a separate coroutine
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.uiState.collect { renderState(it) } }
launch { viewModel.events.collect { handleEvent(it) } }
}
}
// In Compose:
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.events.collect { handleEvent(it) }
}
Error Handling & Lifecycle
15. How do you handle errors in a Flow?
// catch — handles UPSTREAM exceptions
repository.getArticlesFlow()
.map { it.toUiModel() }
.catch { e ->
// Catches errors from getArticlesFlow() and map
emit(emptyList()) // emit fallback value
}
.collect { showArticles(it) }
// ⚠️ catch does NOT catch exceptions in collect
// For collector exceptions, use try-catch around collect
// onCompletion — runs when Flow completes (success, error, or cancel)
flow.onCompletion { cause ->
_isLoading.value = false // cleanup regardless of outcome
}.collect { /* ... */ }
// catch already re-throws CancellationException — no manual handling needed
16. What is repeatOnLifecycle and why is it important?
// ❌ Without repeatOnLifecycle — collection continues in background
lifecycleScope.launch {
viewModel.locationUpdates.collect { updateMap(it) }
// Still collecting when app is in background!
// Wastes battery, may crash if updating destroyed views
}
// ✅ With repeatOnLifecycle — automatically pauses/resumes
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.locationUpdates.collect { updateMap(it) }
}
}
// Goes below STARTED → collection cancelled
// Returns to STARTED → collection restarted
// View destroyed → cancelled permanently
// This is the STANDARD way to collect Flows in Activities/Fragments
17. What is callbackFlow and when do you use it?
// callbackFlow bridges callback-based APIs to Flow
fun locationUpdates(): Flow<Location> = callbackFlow {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
trySend(result.lastLocation) // send from callback into Flow
}
}
locationClient.requestLocationUpdates(request, callback, looper)
awaitClose {
locationClient.removeLocationUpdates(callback) // cleanup
}
}
// Key points:
// - trySend() is non-suspending — safe to call from callbacks
// - awaitClose {} is REQUIRED — runs when the Flow collector cancels
// - The Flow is HOT while being collected (actively listening to callbacks)
// - Use for: location, sensors, Firebase listeners, any callback-based API
Advanced
18. What is the difference between shareIn and stateIn?
// stateIn → produces StateFlow (has .value, replays 1, skips duplicates)
val state: StateFlow<Data> = flow.stateIn(scope, started, initialValue)
// shareIn → produces SharedFlow (configurable replay, delivers all)
val shared: SharedFlow<Event> = flow.shareIn(scope, started, replay = 0)
// stateIn: for UI state that new collectors should see immediately
// shareIn: for events or data where you control replay behaviour
19. What is the difference between Flow and Channel?
// Flow — cold, one-to-many (each collector gets all values)
val flow = flowOf(1, 2, 3)
launch { flow.collect { println("A: $it") } } // gets 1, 2, 3
launch { flow.collect { println("B: $it") } } // gets 1, 2, 3
// Channel — hot, one-to-one (each value goes to ONE receiver)
val channel = Channel<Int>()
launch { for (v in channel) println("A: $v") } // gets some values
launch { for (v in channel) println("B: $v") } // gets the rest
// Flow: data streams for UI (StateFlow, SharedFlow)
// Channel: coroutine-to-coroutine communication, work queues, fan-out
20. How would you implement retry with exponential backoff in Flow?
fun <T> Flow<T>.retryWithBackoff(
maxRetries: Int = 3,
initialDelay: Long = 1000,
maxDelay: Long = 10000
): Flow<T> = retryWhen { cause, attempt ->
if (cause is CancellationException) false // never retry cancellation
else if (attempt < maxRetries) {
val delay = (initialDelay * 2.0.pow(attempt.toInt()))
.toLong().coerceAtMost(maxDelay)
delay(delay)
true
} else false
}
// Usage
repository.getArticlesFlow()
.retryWithBackoff(maxRetries = 3)
.catch { emit(emptyList()) }
.collect { _articles.value = it }
Summary
- Know cold vs hot — Flow is cold (runs on collect), StateFlow/SharedFlow are hot
- Know StateFlow vs SharedFlow — state (has value, skips duplicates) vs events (no value, delivers all)
- Know the key operators — map, filter, combine, zip, flatMapLatest, debounce, distinctUntilChanged
- Understand flowOn — changes upstream context; never use withContext inside flow {}
- Know backpressure strategies — buffer, conflate, collectLatest
- Always collect with repeatOnLifecycle in Activities/Fragments
- Use WhileSubscribed(5000) as the default stateIn strategy
- Use callbackFlow to bridge callback-based APIs to Flow
- Know catch catches upstream only — use try-catch for collector errors
- Launch separate coroutines when collecting multiple Flows
Flow questions test your understanding of reactive programming in Kotlin. The key is knowing which tool to use for which scenario — StateFlow for state, SharedFlow for events, combine for multiple sources, flatMapLatest for search, and repeatOnLifecycle for safe collection. Master these patterns and you’ll handle any Flow question confidently.
Happy coding!
Comments (0)