You are building a live-search feature. Compare collectLatest/flatMapLatest with conflate and explain how debounce fits in. When is collectLatest the wrong choice?
answer
- flatMapLatest = cancel old request, run latest, no stale overwrite
- conflate = keep latest, never cancel running handler
- debounce = wait for typing pause, fewer queries
- Pipeline: debounce → distinctUntilChanged → flatMapLatest
- Wrong for must-complete side effects
basics
~20 sFor live search you want only the newest query to matter. collectLatest/flatMapLatest cancel the previous in-flight search when a new query arrives. conflate instead keeps the latest value but lets the current handler finish without cancelling. debounce waits for a pause in typing before emitting, cutting wasted requests.
solid answer
~50 sIn live search, each keystroke supersedes the prior query. flatMapLatest is the idiomatic operator: it cancels the previous inner request flow and starts a new one, so a slow earlier request is abandoned and won't overwrite fresher results — this also prevents out-of-order responses. collectLatest gives the same cancel-previous behavior at the terminal if you fire the request directly in the block. conflate is different: it never cancels the running handler; it just drops intermediate emissions while the collector is busy and delivers the most recent value when free — good when the work is cheap/non-cancellable but you can't keep up. debounce(timeout) suppresses bursts: it only emits a value after no new value for `timeout`, drastically reducing requests while typing. Typical pipeline: queries.debounce(300).distinctUntilChanged().flatMapLatest { search(it) }. collectLatest is wrong when the in-flight work has side effects that must complete (a payment, an analytics write) — cancelling mid-flight could leave partial state.
code
kotlin · 12 linesimport kotlinx.coroutines.flow.*
fun searchResults(queries: Flow<String>, repo: SearchRepo): Flow<List<Hit>> =
queries
.debounce(300)
.map { it.trim() }
.filter { it.length >= 2 }
.distinctUntilChanged()
.flatMapLatest { q -> repo.search(q) } // cancels previous query
interface SearchRepo { fun search(q: String): Flow<List<Hit>> }
class Hitgo deeper
Knows flatMapLatest/collectLatest cancel the previous search for live search.
Builds the debounce → distinctUntilChanged → flatMapLatest pipeline and explains each stage.
Contrasts conflate (no cancel) vs Latest (cancel), and articulates the out-of-order/stale-overwrite guarantee.
Identifies when cancellation is unsafe (side effects, non-idempotent work) and designs an alternative (queue, collect, idempotency keys).
## The problem Live search emits a query per keystroke; only the **latest** query's results matter, and we want to minimize wasted network calls and avoid **out-of-order results** (a slow old request landing after a newer one). ## The three tools - **`collectLatest` / `flatMapLatest`** — *cancel the previous in-flight processing* when a newer value arrives. `flatMapLatest { q -> searchFlow(q) }` cancels the previous inner request flow's collection and starts the new query. This both saves the wasted request and guarantees the displayed result corresponds to the latest query. - **`conflate()`** — *no cancellation*. While the collector is busy, upstream values are buffered to capacity 1, keeping only the **most recent**; intermediate values are dropped. The current handler always finishes. Choose this when the per-value work is short and cannot/should-not be cancelled, but you still can't process every value. - **`debounce(timeoutMillis)`** — *time-based gating*. Emits a value only after `timeout` elapses with no newer value, collapsing a burst of keystrokes into one emission. It reduces how many queries even reach the search at all. ## How they compose ```kotlin val results: Flow<List<Hit>> = queryFlow .debounce(300) // wait for a typing pause .filter { it.length >= 2 } // skip trivial queries .distinctUntilChanged() // ignore no-op changes .flatMapLatest { q -> // cancel previous request on new query searchRepository.search(q) } ``` - `debounce` cuts the *number* of queries. - `distinctUntilChanged` removes redundant repeats. - `flatMapLatest` ensures only the **latest** query's request runs to completion. ## collectLatest vs flatMapLatest here Both cancel previous work. Use `flatMapLatest` when the search itself is a `Flow` (e.g. streaming or paged results) so you can chain operators on the result. Use `collectLatest` when you just fire a `suspend` call and update state in the terminal. ## When collectLatest is the wrong choice - **Side-effecting, must-complete work:** cancelling a half-done write, payment, file move, or non-idempotent mutation can leave partial state. Prefer `collect` (handle every value) or a queue. - **Every value matters** (audit logs, metrics): cancellation drops processing of superseded values — unacceptable. - **Non-cancellable work** (no suspension point): cancellation won't fire, so you gain nothing and may pile up. - For those, `conflate` (finish current, skip stale) or plain `collect` are safer. ## Out-of-order safety A subtle senior point: with plain `collect` + launching requests concurrently, a slow old request can resolve after a fast new one and overwrite the UI with stale data. `flatMapLatest`/`collectLatest` eliminate this by cancelling the old request entirely.
- How does flatMapLatest prevent stale results overwriting fresh ones?It cancels the previous inner flow's collection when a new query arrives, so the old request's results never get emitted/applied.
- Why pair debounce with distinctUntilChanged?debounce reduces burst frequency; distinctUntilChanged removes consecutive duplicate queries (e.g. text changed then reverted), avoiding redundant identical searches.
- When would you pick conflate over collectLatest?When the per-value work is cheap and must finish (non-cancellable or side-effecting) but you still can't keep up — conflate keeps only the latest without cancelling the running handler.
debounce is waiting until someone stops talking before you reply; flatMapLatest is hanging up the old call the instant a new one rings.
saying these in an interview costs you the question
- Using collectLatest for must-complete side-effecting work
- Claiming conflate cancels the in-flight handler (it doesn't)
- Thinking debounce and flatMapLatest are interchangeable
- Ignoring the out-of-order/stale-result hazard that flatMapLatest solves
- Forgetting distinctUntilChanged, causing duplicate searches