How to Avoid Multiple onClick Events on Jetpack Compose Buttons

2023/06/04

Problem

If you have worked with Jetpack Compose, you may have encountered the issue of multiple onClick events being triggered by a single button press. This can lead to unexpected behavior and crashes, frustrating both you and your users. In this article, we will explore solutions to this issue and ensure that your buttons function as expected.

Solution

To solve this problem, you can create your own button wrapper. This button’s onClick logic can only be run once. This is useful if you want to use your button for navigation or other tasks where you only want its onClick logic to be executed once.


_34
@Composable
_34
fun ClickOnceButton(
_34
onClick: () -> Unit,
_34
modifier: Modifier = Modifier,
_34
conditional: () -> Boolean = {true},
_34
enabled: Boolean = true,
_34
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
_34
elevation: ButtonElevation? = ButtonDefaults.elevation(),
_34
shape: Shape = MaterialTheme.shapes.small,
_34
border: BorderStroke? = null,
_34
colors: ButtonColors = ButtonDefaults.buttonColors(),
_34
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
_34
content: @Composable RowScope.() -> Unit
_34
) {
_34
var onClickHasExecuted by remember { mutableStateOf(false) }
_34
_34
Button(
_34
onClick = {
_34
if (conditional() && !onClickHasExecuted) {
_34
onClickHasExecuted = true
_34
onClick()
_34
}
_34
},
_34
modifier = modifier,
_34
enabled = enabled,
_34
interactionSource = interactionSource,
_34
elevation = elevation,
_34
shape = shape,
_34
border = border,
_34
colors = colors,
_34
contentPadding = contentPadding,
_34
content = content
_34
)
_34
}

Navigation Example

In this example, the button will only navigate the first time it is clicked, avoiding multiple navigations to the same route:


_10
@Composable
_10
fun Example(navController: NavController = rememberNavController()){
_10
ClickOnceButton(onClick = {
_10
val someRoute = "some_route"
_10
navController.navigate(someRoute)
_10
}) {
_10
Text(text = "Click me!")
_10
}
_10
}

Conditional Example

Now, let’s explain the conditional parameter in the wrapper. Sometimes, you may only want the logic to execute if something else is true. This optional parameter is necessary because if you implement the condition in the onClick delegate, the button won't be clickable again, even if the condition would return true on further clicks.

It’s probably easier to understand if you see it in action:


_17
@Composable
_17
fun Example(navController: NavController = rememberNavController()) {
_17
var someBoolean by remember { mutableStateOf(false) }
_17
_17
Button(onClick = { someBoolean = !someBoolean }) {
_17
Text(text = "This toggles the boolean!")
_17
}
_17
_17
ClickOnceButton(
_17
conditional = { someBoolean },
_17
onClick = {
_17
val someRoute = "some_route"
_17
navController.navigate(someRoute)
_17
}) {
_17
Text(text = "Click me!")
_17
}
_17
}

In this example, it is not possible for the ClickOneButton to run its logic in the initial state. However, if we click the normal Button, the condition that ClickOneButton depends on is switched, making it possible to run its logic.

Conclusion

In conclusion, by creating a button wrapper with onClick logic that can only be executed once, we can ensure that our buttons behave as expected and avoid the frustrating issue of multiple onClick events being triggered by a single button press. By using the conditional parameter, we can further customize our button's behavior, making it a powerful tool for navigating and interacting with our app.