Skip to Content
Course content

205: Whiteboard Practice: Implementing a Debounce Function with Coroutines

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've ever built a search bar, you know the struggle. You want the app to feel responsive by fetching results as the user types, but you can't exactly fire off a network request for every single character. If a user types "Kotlin Coroutines" quickly, you don't want 17 simultaneous API calls hitting your backend. You want to wait until the user has paused for a moment—that's debouncing.

The "Delayed Flood" approach

When I first started playing with Coroutines, I tried a naive approach that felt right in my head but was a disaster in practice. I thought, "I'll just launch a coroutine that waits 300ms and then makes the call." It looked something like this:

fun onTextChanged(text: String) {
    scope.launch {
        delay(300)
        searchApi.fetchResults(text)
    }
}

Here is where it breaks: this isn't actually debouncing. This is just delaying the flood. If the user types five characters in 200ms, you've just scheduled five separate coroutines. After 300ms, all five of those delays expire almost simultaneously, and your API gets slammed with five requests anyway. Worse, because of network latency, the result for the first character might arrive after the result for the fifth character, causing your UI to flicker back to an old search result. I've seen this bug haunt a few junior devs, and it's a great example of why "asynchronous" doesn't automatically mean "coordinated."

Managing state with Job cancellation

To actually debounce, we need a way to say, "If a new event comes in, the previous one is now irrelevant. Kill it." In Kotlin, this means we need to keep a reference to the current Job. By canceling the previous job before starting a new one, we ensure that only the very last keystroke—the one that finally gets a 300ms window of silence—actually triggers the API call.

I prefer implementing this as a reusable pattern. While you could use Flow.debounce() in a production app, when you're in a whiteboard interview, the interviewer usually wants to see if you understand the underlying mechanics of Job management. Here is how I'd write a clean, manual implementation:

class SearchHandler(private val scope: CoroutineScope) {
    private var searchJob: Job? = null

    fun onTextChanged(text: String) {
        // Cancel the previous search if it's still waiting to execute
        searchJob?.cancel()

        searchJob = scope.launch {
            delay(300) 
            // If we reach this line, it means the user stopped typing for 300ms
            val results = searchApi.fetchResults(text)
            updateUi(results)
        }
    }
}

The trade-off here is simple: we're trading a tiny bit of memory to hold onto that Job reference in exchange for massive efficiency in network usage and UI stability. You're effectively resetting a timer every time the user interacts. If they type 100 characters without pausing, you only ever pay the cost of one API call.

One thing to keep in mind: cancel() is cooperative. Because we're using delay(), which is a suspending function, it checks for cancellation automatically. If we were doing some heavy CPU-bound work inside that block without calling any suspending functions, cancel() wouldn't actually stop the work immediately. Always remember that Coroutines don't just "kill" a thread; they ask the code to stop politely.




📋 Practical Task

Implement a Debounced Window-Resize Listener

Imagine you are building a dashboard where a complex chart needs to be re-calculated and re-drawn whenever the browser or app window is resized. Recalculating the chart is expensive, and window resize events fire dozens of times per second.

Create a class called ChartResizeHandler that takes a CoroutineScope in its constructor. Implement a function onResize(width: Int, height: Int) that uses the Job-cancellation pattern to ensure that the recalculateChart(w, h) function is only called after the user has stopped resizing the window for at least 500 milliseconds.

Requirements:

  • Maintain a reference to the active resize job.
  • Cancel any existing job when a new resize event arrives.
  • Use delay(500) to create the debounce window.
  • Ensure the recalculateChart call is wrapped within the coroutine.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.