Skip to content

Latest commit

 

History

History
120 lines (91 loc) · 4.52 KB

File metadata and controls

120 lines (91 loc) · 4.52 KB

Architecture: SearchWithDebounce

A small Jetpack Compose app that searches a local list only after the user pauses typing, and cancels any in-flight search when a newer query arrives.

1. The problem being solved

Typing into a search box produces a new value on every keystroke (B → Ba → Baj → Baji). Hitting a data source for each of those values is wasteful:

  • Most intermediate queries are discarded immediately.
  • Slow requests can finish out of order and show stale results.
  • The UI feels busy (loading spinners) while the user is still typing.

This app demonstrates how Kotlin Flow solves that with debounce, distinctUntilChanged, and flatMapLatest.

2. Complete data flow

Compose TextField
        │
        │  onQueryChanged(query)
        ▼
SearchViewModel
        │
        │  MutableStateFlow<String>   ← query updates instantly (UI stays in sync)
        ▼
debounce(500)                         ← wait until typing pauses
        │
distinctUntilChanged()                ← skip duplicate settled queries
        │
flatMapLatest { query ->              ← cancel the previous search Flow
    Repository.search(query)              (including its delay)
}
        │
FakeSearchRepository                  ← delay(2000) + in-memory filter
        │
StateFlow<SearchUiState>              ← Initial | Loading | Success | Empty | Error
        │
        ▼
Compose UI

Unidirectional data flow: the UI only sends events (onQueryChanged, retry). The ViewModel owns state. The UI never talks to the repository.

ui/
├── SearchScreen.kt      ← observes StateFlows, renders one of the UI states
├── SearchViewModel.kt   ← query StateFlow + search pipeline
└── SearchUiState.kt     ← sealed UI states

data/
├── SearchRepository.kt      ← suspend search contract
└── FakeSearchRepository.kt  ← static list + simulated latency + log cancellation

3. Why debounce is required

debounce(500) emits a query only when no new value arrived for 500 ms.

Without it, B, Ba, Baj, and Baji would each start a search. With it, only Search("Baji") runs after the user pauses.

The TextField itself is not debounced: _query updates on every keystroke so the typed text appears immediately. Only the search waits.

4. Why cancellation is required

The fake API uses delay(2000) so a search is easy to interrupt. If the user changes the query while a request is in flight:

SEARCH STARTED: Android
SEARCH CANCELLED: Android     ← delay() is cancelled; work is not finished
SEARCH STARTED: Kotlin
SEARCH COMPLETED: Kotlin

Without cancellation, Android could still complete after Kotlin and overwrite the UI with the wrong results.

Cancellation is cooperative: delay() checks the coroutine job. flatMapLatest cancels that job when a new query arrives.

5. What happens when the user types quickly

t=0ms     type "A"
t=80ms    type "An"
t=160ms   type "And"
t=240ms   type "Andr"
t=320ms   type "Android"
          debounce timer resets on every keystroke
t=820ms   500 ms of silence → emit "Android"
          flatMapLatest starts search
          UI → Loading

No repository call happens at t=0, 80, 160, 240, or 320. Intermediate queries never leave the ViewModel.

6. What happens when a previous request is already running

t=820ms    Search("Android") starts, delay(2000) begins
t=1200ms   user replaces text with "Kotlin"
           query StateFlow updates immediately (TextField shows "Kotlin")
           debounce timer starts again
t=1700ms   500 ms of silence → emit "Kotlin"
           flatMapLatest cancels the Android coroutine
           delay(2000) for Android throws CancellationException
           Search("Kotlin") starts
t=3700ms   Kotlin completes → Success or Empty

Filter Logcat by SearchDebounce to see STARTED / CANCELLED / COMPLETED.

7. Why flatMapLatest is appropriate

flatMapLatest switches to the new inner Flow and cancels the previous one.

Operator Behavior Wrong for search because
flatMapConcat Waits for the previous inner Flow to finish, then runs the next Stale "Android" results can appear after the user already typed "Kotlin"
flatMapMerge Runs inner Flows concurrently Completions can arrive out of order
flatMapLatest Cancels the previous inner Flow, runs only the latest Latest query wins; in-flight work is dropped

Search is last-write-wins. flatMapLatest is the operator that matches that rule.