Handling user interactions

User interface components give feedback to the device user by the way they respond to user interactions. Every component has its own way of responding to interactions, which helps the user know what their interactions are doing. For example, if a user touches a button on a device's touchscreen, the button is likely to change in some way, perhaps by adding a highlight color. This change lets the user know that they touched the button. If the user didn't want to do that, they'll know to drag their finger away from the button before releasing–otherwise, the button will activate.

The Compose Gestures documentation covers how Compose components handle low-level pointer event, such as pointer moves and clicks. Out of the box, Compose abstracts those low-level events into higher-level interactions–for example, a series of pointer events might add up to a button press-and-release. Understanding those higher-level abstractions can help you customize how your UI responds to the user. For example, you might want to customize how a component's appearance changes when the user interacts with it, or maybe you just want to maintain a log of those user actions. This document gives you the information you need to modify the standard UI elements, or design your own.

Interactions

In many cases, you don't need to know just how your Compose component is interpreting user interactions. For example, Button relies on Modifier.clickable to figure out whether the user clicked the button. If you're adding a typical button to your app, you can define the button's onClick code, and Modifier.clickable runs that code when appropriate. That means you don't need to know whether the user tapped the screen or selected the button with a keyboard; Modifier.clickable figures out that the user performed a click, and responds by running your onClick code.

However, if you want to customize your UI component's response to user behavior, you may need to know more of what's going on under the hood. This section gives you some of that information.

When a user interacts with a UI component, the system represents their behavior by generating a number of Interaction events. For example, if a user touches a button, the button generates PressInteraction.Press. If the user lifts their finger inside the button, it generates a PressInteraction.Release, letting the button know that the click was finished. On the other hand, if the user drags their finger outside the button, then lifts their finger, the button generates PressInteraction.Cancel, to indicate that the press on the button was canceled, not completed.

These interactions are unopinionated. That is, these low-level interaction events don't intend to interpret the meaning of the user actions, or their sequence. They also don't interpret which user actions might take priority over other actions.

These interactions generally come in pairs, with a start and an end. The second interaction contains a reference to the first one. For example, if a user touches a button then lifts their finger, the touch generates a PressInteraction.Press interaction, and the release generates a PressInteraction.Release; the Release has a press property identifying the initial PressInteraction.Press.

You can see the interactions for a particular component by observing its InteractionSource. InteractionSource is built on top of Kotlin flows, so you can collect the interactions from it the same way you'd work with any other flow.

Interaction state

You might want to extend the built-in functionality of your components by also tracking the interactions yourself. For example, perhaps you want a button to change color when it's pressed. The simplest way to track the interactions is to observe the appropriate interaction state. InteractionSource offers a number of methods that reveal various interaction statuses as state. For example, if you want to see whether a particular button is pressed, you can call its InteractionSource.collectIsPressedAsState() method:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(
    onClick = { /* do something */ },
    interactionSource = interactionSource) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

Besides collectIsPressedAsState(), Compose also provides collectIsFocusedAsState(), collectIsDraggedAsState(), and collectIsHoveredAsState(). These methods are actually convenience methods built on top of lower-level InteractionSource APIs. In some cases, you may want to use those lower-level functions directly.

For example, suppose you need to know whether a button is being pressed, and also whether it's being dragged. If you use both collectIsPressedAsState() and collectIsDraggedAsState(), Compose does a lot of duplicate work, and there's no guarantee you'll get all the interactions in the right order. For situations like this, you might want to work directly with the InteractionSource. The following section describes how you can track the interactions yourself, getting just the information you need.

Work with InteractionSource

If you need low-level information about interactions with a component, you can use standard flow APIs for that component's InteractionSource. For example, suppose you want to maintain a list of the press and drag interactions for an InteractionSource. This code does half the job, adding the new presses to the list as they come in:

val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
        }
    }
}

But besides adding the new interactions, you also have to remove interactions when they end (for example, when the user lifts their finger back off the component). That's easy to do, since the end interactions always carry a reference to the associated start interaction. This code shows how you'd remove the interactions that have ended:

val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is PressInteraction.Release -> {
                interactions.remove(interaction.press)
            }
            is PressInteraction.Cancel -> {
                interactions.remove(interaction.press)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
            is DragInteraction.Stop -> {
                interactions.remove(interaction.start)
            }
            is DragInteraction.Cancel -> {
                interactions.remove(interaction.start)
            }
        }
    }
}

Now, if you want to know if the component is currently being pressed or dragged, all you have to do is check whether interactions is empty:

val isPressedOrDragged = interactions.isNotEmpty()

If you want to know what the most recent interaction was, just look at the last item in the list. For example, this is how the Compose ripple implementation figures out the appropriate state overlay to use for the most recent interaction:

val lastInteraction = when (interactions.lastOrNull()) {
    is DragInteraction.Start -> "Dragged"
    is PressInteraction.Press -> "Pressed"
    else -> "No state"
}

Working through an example

To see how you can build components with a custom response to input, here's an example of a modified button. In this case, suppose you want a button that responds to presses by changing its appearance:

Animation of a button that dynamically adds an icon when clicked

To do this, build a custom composable based on Button, and have it take an additional icon parameter to draw the icon (in this case, a shopping cart). You call collectIsPressedAsState() to track whether the user is hovering over the button; when they are, you add the icon. Here's what the code looks like:

@Composable
fun PressIconButton(
    onClick: () -> Unit,
    icon: @Composable () -> Unit,
    text: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    interactionSource: MutableInteractionSource =
        remember { MutableInteractionSource() },
) {
    val isPressed by interactionSource.collectIsPressedAsState()
    Button(onClick = onClick, modifier = modifier,
        interactionSource = interactionSource) {
        AnimatedVisibility(visible = isPressed) {
            if (isPressed) {
                Row {
                    icon()
                    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                }
            }
        }
        text()
    }
}

And here's what it looks like to use that new composable:

PressIconButton(
    onClick = {},
    icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) },
    text = { Text("Add to cart") }
)

Because this new PressIconButton is built on top of the existing Material Button, it reacts to user interactions in all the usual ways. When the user presses the button, it changes its opacity slightly, just like an ordinary Material Button. In addition, thanks to the new code, the HoverIconButton dynamically responds to the hover by adding an icon.