Debouncing Clicks in Compose

Debouncing Clicks in Compose

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