Skip to content

Repository files navigation

SearchWithDebounce

A small Kotlin + Jetpack Compose app that demonstrates debounced search, Kotlin Flow, StateFlow, flatMapLatest, and cancellation of in-flight requests.

There is no real network call. A fake repository filters a static list and uses delay(2000) so cancellation is easy to see in Logcat.

Primary color: #6200EE. The screen uses a light purple gradient behind the same search layout (field on top, results below).


Prerequisites

  • Android Studio Otter or newer (the project uses AGP 9.3 and Kotlin 2.2)
  • JDK 11 or newer (Android Studio’s bundled JDK is enough)
  • An Android emulator (API 24+) or a physical device with USB debugging
  • Internet on first Gradle sync so dependencies can download

Setup

1. Open the project

  1. Launch Android Studio.
  2. File → Open and select this folder (SearchApp).
  3. Wait for Gradle sync to finish. If prompted, use the Gradle JDK bundled with Android Studio.

From the command line:

cd "/path/to/SearchApp"
./gradlew assembleDebug

On Windows use gradlew.bat instead of ./gradlew.

2. Run the app

Android Studio

  1. Start an emulator or connect a device.
  2. Select the app run configuration.
  3. Click Run (or Shift+F10 / Control+R).

Command line

./gradlew installDebug

Then open SearchWithDebounce on the device.

3. Watch cancellation in Logcat

  1. Open the Logcat tool window.
  2. Filter by tag: SearchDebounce
  3. Use the scenarios below.

Problem

Calling an API on every keystroke is inefficient.

If the user types Baji, the app would otherwise fire four requests:

Search("B")
Search("Ba")
Search("Baj")
Search("Baji")

Most of those queries are thrown away a moment later. They still cost battery, bandwidth, and backend work. A slow response for "B" can also arrive after "Baji" and show the wrong results.


Solution

The ViewModel does not search on every keystroke. It runs this pipeline:

debounce
    ↓
Wait until user stops typing

distinctUntilChanged
    ↓
Avoid duplicate searches

flatMapLatest
    ↓
Cancel previous search when new query arrives

debounce(500)

Emits a query only after 500 ms with no new input. Typing BBaBajBaji quickly results in a single Search("Baji").

The TextField still updates instantly. Only the search waits.

distinctUntilChanged()

If the settled query is the same as the last one (for example the user types Kotlin, deletes a letter, then types it back), the extra search is skipped.

flatMapLatest()

Starts a new search Flow for the latest query and cancels the previous one. Cancellation reaches delay() in the fake repository, so the old request does not complete and cannot overwrite the UI.


Why use flatMapLatest instead of flatMapConcat?

flatMapConcat waits for the previous inner Flow to finish before starting the next one.

That is wrong for search:

User types "Android"  →  search starts (2s delay)
User types "Kotlin"   →  queued behind Android

Android completes     →  UI shows Android   ← stale
Kotlin starts         →  UI later shows Kotlin

The user already moved on, but outdated results still appear.

flatMapLatest gives the latest query priority. When "Kotlin" arrives, the "Android" Flow is cancelled. Only Kotlin’s result can reach the UI:

SEARCH STARTED: Android
SEARCH CANCELLED: Android
SEARCH STARTED: Kotlin
SEARCH COMPLETED: Kotlin

flatMapMerge would run both at once and could still deliver Android last. Search is last-write-wins; flatMapLatest is the matching operator.


How to try cancellation

Time →

User types:

A → An → And → Andr → Android
                        ↓
                    wait 500ms
                        ↓
                Search Android starts
                        ↓
                  delay 2000ms

User types:

Kotlin
   ↓

Android request cancelled
   ↓
wait 500ms
   ↓
Kotlin search starts
   ↓
Kotlin results displayed

Steps:

  1. Filter Logcat by SearchDebounce.
  2. Type Android, wait until Searching... appears, then quickly replace the text with Kotlin.

Expected logs:

SEARCH STARTED: Android
SEARCH CANCELLED: Android
SEARCH STARTED: Kotlin
SEARCH COMPLETED: Kotlin

The Kotlin match is shown. Android never appears after cancellation.

Other states

What you type What you should see
(empty) Start typing to search
kot Kotlin
flow Flow
xyz No results found
error Something went wrong + Retry

Catalog items: Android, Kotlin, Jetpack Compose, Coroutines, Flow, Room, Retrofit, Hilt, WorkManager, Firebase.


Architecture

Compose TextField
        │
        │  onQueryChanged(query)
        ▼
SearchViewModel
        │
        │  MutableStateFlow<String>   ← query updates instantly
        ▼
debounce(500)
        │
distinctUntilChanged()
        │
flatMapLatest { query ->              ← cancels the previous search
    Repository.search(query)
}
        │
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.

Timing diagrams and operator comparisons are in ARCHITECTURE.md.


Project layout

app/src/main/java/com/search/app/
├── MainActivity.kt
├── ui/
│   ├── SearchScreen.kt       Compose UI (gradient + search states)
│   ├── SearchViewModel.kt    debounce / distinctUntilChanged / flatMapLatest
│   ├── SearchUiState.kt      Initial, Loading, Success, Empty, Error
│   └── theme/                Material 3 purple theme (#6200EE)
└── data/
    ├── SearchRepository.kt
    └── FakeSearchRepository.kt

No extra layers (use cases, Hilt, or a real API). The goal is to see how Flow cancellation and debounce behave in a real Android screen.


Theme

  • Primary: #6200EE (Material Purple 500)
  • Gradient: a light lilac wash at the top that eases to white (a deeper purple wash in dark theme)
  • UX: unchanged — search field, then Initial / Loading / results / Empty / Error + Retry

Dynamic (wallpaper) color is disabled so #6200EE stays the primary on every device.


Tech stack

Piece Choice
Language Kotlin 2.2
UI Jetpack Compose, Material 3
State StateFlow + ViewModel
Async Kotlin Coroutines + Flow
Min SDK 24
Target / compile SDK 37

About

SearchWithDebounce is a Kotlin Compose demo that searches a local list 500 ms after typing stops, cancels in-flight requests with flatMapLatest, and shows loading, success, empty, and error states.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages