androidandroid-jetpack-compose

Implement debounce click suffer Unnecessary use of Modifier.composed?


I'm trying to create a debounce clickable Compose Modifier as code snippet below.

inline fun Modifier.debounceClickable(
    debounceInterval: Long = 400,
    crossinline onClick: () -> Unit,
): Modifier = composed {
    var lastClickTime = 0L
    clickable() {
        val currentTime = System.currentTimeMillis()
        if ((currentTime - lastClickTime) < debounceInterval) return@clickable
        lastClickTime = currentTime
        onClick()
    }
}

Not sure why the IDE warned Unnecessary use of Modifier.composed ?

BTW, is this debounce implement got any potential danger or leak ?


Solution

  • You only need to use composed to make @Composable calls.

    Note, that inside composed you should only use state annotated composables, like to save some state using remember, but not the composable views. More details can be found here.

    So technically, to get rid of the warning, you could've simplified your code to this:

    inline fun Modifier.debounceClickable(
        debounceInterval: Long = 400,
        crossinline onClick: () -> Unit,
    ): Modifier {
        var lastClickTime = 0L
        return clickable() {
            val currentTime = System.currentTimeMillis()
            if ((currentTime - lastClickTime) < debounceInterval) return@clickable
            lastClickTime = currentTime
            onClick()
        }
    }
    

    And it would work the same, as your current code.

    The problem is that you store data in lastClickTime, and with your code it's gonna be reset as soon as the first recomposition happens. It may work fine only if you don't change UI with the action.

    You can test it with the following core:

    var flag by remember { mutableStateOf(false) }
    Text("clickable $flag", Modifier.debounceClickable { flag = !flag } )
    

    That's why you actually need composed to store this value using remember and mutableStateOf, just like in any other view:

    inline fun Modifier.debounceClickable(
        debounceInterval: Long = 400,
        crossinline onClick: () -> Unit,
    ): Modifier = composed {
        var lastClickTime by remember { mutableStateOf(0L) }
        clickable() {
            val currentTime = System.currentTimeMillis()
            if ((currentTime - lastClickTime) < debounceInterval) return@clickable
            lastClickTime = currentTime
            onClick()
        }
    }
    

    I suggest you check out with state in Compose documentation, including this youtube video which explains the basic principles.