Debouncing Clicks in Compose

Search for a command to run...

No comments yet. Be the first to comment.
I've been a professional software developer in the mobile space for around 15 years now. I started in the Android 2.1 / Eclipse / Ant days and iOS Objective-C days pre-ARC on Xcode 4 (if memory servic

Intro I recently learned about friendPaths as part of the Kotlin Gradle Plugin while browsing an Android Slack group. From the documentation on friendPaths: "Paths to the output directories of the friend modules whose internal declarations should be ...

If you've ever tried to compare the behavior of Row in Google's Jetpack Compose, HStack in Apple's SwiftUi, and something like display: flex in the web world, you'll see some interesting results. First, let's take a look at how HStack renders two lon...

Said another way, how to supercharge your development workflow with a debug drawer. As Android developers, we're constantly looking for ways to streamline our workflow. One often-overlooked tool that

As Jetpack Compose becomes more widely used across Android (and Multiplatform projects!), detecting regressions via tooling becomes more important to shift left and detect regressions earlier in the d

I recently needed to add a debouncing operator on my button clicks in a Compose multiplatform project. I thought I'd share my solution in case it was useful to someone else.
First, we define an EventProcessor interface. The concrete implementation will be responsible for debouncing our clicks.
internal interface EventProcessor {
fun processEvent(event: () -> Unit)
companion object {
val buttonClickMap = mutableMapOf<String, EventProcessor>()
}
}
The implementation:
private const val DEBOUNCE_TIME_MILLIS = 1000L
private class EventProcessorImpl : EventProcessor {
private val now: Long
// this is being used in Compose multiplatform
// switch out with whatever millisecond provider you want
get() = Clock.System.now().toEpochMilliseconds()
private var lastEventTimeMs: Long = 0
override fun processEvent(event: () -> Unit) {
if (now - lastEventTimeMs >= DEBOUNCE_TIME_MILLIS) {
event.invoke()
}
lastEventTimeMs = now
}
}
The Composable function where our EventProcessor will be used and a helper function for getting this button's EventProcessor
internal fun EventProcessor.Companion.get(id: String): EventProcessor {
return buttonClickMap.getOrPut(
id
) {
EventProcessorImpl()
}
}
@Composable
fun debouncedClick(
id: String = randomUUID(),
onClick: () -> Unit,
): () -> Unit {
val multipleEventsCutter = remember { EventProcessor.get(id) }
val newOnClick: () -> Unit = {
multipleEventsCutter.processEvent { onClick() }
}
return newOnClick
}
Here is an example of it being used:
PrimaryButton(
onClick = debouncedClick {
// handle click
},
) {
Text("Button")
}
Full code:
https://gist.github.com/j-roskopf/990baa5beef767fbb2fae8cce33e2529