Apply SwipeToDismissBox in Android Jetpack Compose
Tags: androiddev, jetpackcompose, swipetodismiss
Published: 2024-05-02 08:48:17

Here, let’s talk about the **SwipeToDismissBox** Composable which is more compose way to apply swipe to dismiss.

I hope you have seen the swipe feature in the Google’s Gmail app. It is a very handy feature which saves a lot of taps (clicks). Today, we will be implementing something like that using **SwipeToDismissBox** composable.


![Swipe Representation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ltk75lbksa7ojy0nw7vq.gif)


##SwipeToDismissBox Composable

Firstly, I will introduce you to the different parameters that **SwipeToDismissBox** composable takes,

```
@Composable
@ExperimentalMaterial3Api
fun SwipeToDismissBox(
    state: SwipeToDismissBoxState,
    backgroundContent: @Composable RowScope.() -> Unit,
    modifier: Modifier = Modifier,
    enableDismissFromStartToEnd: Boolean = true,
    enableDismissFromEndToStart: Boolean = true,
    content: @Composable RowScope.() -> Unit,
) {...}
```

- **state**: The state of this component.
- **backgroundContent**: A composable that is stacked behind the main content and is visible when the main content is swiped or is being swiped.
- **enableDismissFromStartToEnd**: Whether SwipeToDismissBox can be dismissed from start(left) to end(right).
- **enableDismissFromEndToStart**: Whether SwipeToDismissBox can be dismissed from end(right) to start(left).
- **content**: Our main content that can be dismissed.

## SwipeToDismissBoxState

To achieve the behavior we want, we really need to play with SwipeToDismissBoxState. Thanks to developers behind this, we have very little to no things to worry about.

The **rememberSwipeToDismissBoxState** helps us remember and control the different SwipeToDismissBoxState values.

```
@Composable
@ExperimentalMaterial3Api
fun rememberSwipeToDismissBoxState(
    initialValue: SwipeToDismissBoxValue = SwipeToDismissBoxValue.Settled,
    confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true },
    positionalThreshold: (totalDistance: Float) -> Float =
        SwipeToDismissBoxDefaults.positionalThreshold,
): SwipeToDismissBoxState {...}

```
Peeking into the parameters,
- **initialValue**: It is the initial value of the state.
- **confirmValueChange**: Optional callback for confirming or overriding a pending state change.
- **positionalThreshold**: The distance from the start of a swipe where the target state is calculated. It’s added or subtracted from the origin offset based on swipe direction and is always a positive value.

Let’s understand its use with an example.

I have created a SwipeBox composable function to isolate it from other composables and make it reusable (in my case).

```
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SwipeBox(
    modifier: Modifier = Modifier,
    onDelete: () -> Unit,
    onEdit: () -> Unit,
    content: @Composable () -> Unit
) {
    val swipeState = rememberSwipeToDismissBoxState()

    lateinit var icon: ImageVector
    lateinit var alignment: Alignment
    val color: Color

    when (swipeState.dismissDirection) {
        SwipeToDismissBoxValue.EndToStart -> {
            icon = Icons.Outlined.Delete
            alignment = Alignment.CenterEnd
            color = MaterialTheme.colorScheme.errorContainer
        }

        SwipeToDismissBoxValue.StartToEnd -> {
            icon = Icons.Outlined.Edit
            alignment = Alignment.CenterStart
            color =
                Color.Green.copy(alpha = 0.3f)

        SwipeToDismissBoxValue.Settled -> {
            icon = Icons.Outlined.Delete
            alignment = Alignment.CenterEnd
            color = MaterialTheme.colorScheme.errorContainer
        }
    }
...
```

Here, I initialized some variables to store the icon, alignment property, and color which is assigned based on the **dissmissDirection**.

**state.dismissDirection** gives us the direction in which the composable is being dismissed. Using this we change the background of the SwipeToDismissBox for different actions i.e. on swipe left, and on swipe right.

To understand this more clearly, there are three directions (dragAnchors to be more specific) for dismissing by swipe. They are, **StartToEnd**, **EndToStart**, and **Settled**.

So, when the **dismissDirection** is in progress of any of three position we can call the composable that we want in the background. In our case, we assign the corresponding values for the icons, alignment property, and color, and then these properties are used in the backgroundContent below:

(You can directly call the composables unlike this case.)

```
...
SwipeToDismissBox(
        modifier = modifier.animateContentSize(),
        state = swipeState,
        backgroundContent = {
            Box(
                contentAlignment = alignment,
                modifier = Modifier
                    .fillMaxSize()
                    .background(color)
            ) {
                Icon(
                    modifier = Modifier.minimumInteractiveComponentSize(),
                    imageVector = icon, contentDescription = null
                )
            }
        }
    ) {
        content()
    }
...
```

Here, we set up the **SwipeToDismissBox** composable. I passed **swipeState** as state that I initialized before, and backgroundContent which is the composable that we want behind the main content. content() composable is the main content of our SwipeToDismissBox that we’ll get as an argument in our SwipeBox function.

Now, we would like to execute certain actions if the swipeState changes its positional value i.e. when the swipe is completed. In our case we’ll apply the onDelete() and onEdit() actions on **dismissFromEndToStart** and **dismissFromStartToEnd** respectively. We do this using the state’s .currentValue property which gives us the current state value.

```
...
    when (swipeState.currentValue) {
        SwipeToDismissBoxValue.EndToStart -> {
            onDelete()
        }

        SwipeToDismissBoxValue.StartToEnd -> {
            LaunchedEffect(swipeState) {
                onEdit()
                swipeState.snapTo(SwipeToDismissBoxValue.Settled)
            }
        }

        SwipeToDismissBoxValue.Settled -> {
        }
    }
}
```

As the code above speaks for itself, **swipeState.currentValue** property have three states i.e. **EndToStart**, **StartToEnd**, and **Settled**. When the swipeState is in any of the three state we can apply respective actions on them.

Here, when the **EndToStart** swipe completes I want to apply onDelete() action. Similarly, when the **StartToEnd** swipe completes I want to apply onEdit() action, and then set the state to the Settled position using snapTo function which takes the targetValue as an argument.

That’s it, our desired behavior has been achieved. This is just one way of doing it in countless others to achieve this behavior. There is always better way to do it.

Adding with it, there are many things to play with state such as:
- **progress()** to get the progress status i.e. start 0f..1f end
- **targetValue()** to get the closest state.
- **reset()** to reset the component to default position.
- **dismiss()** to dismiss the component in the given direction.

### Complete Implementation

```
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DemoSwipeToDismiss(
    modifier: Modifier = Modifier
) {
    var myList by remember {
        mutableStateOf((1..3).toList())
    }

    LazyColumn(modifier = modifier) {
        items(myList) { item ->
            SwipeBox(
                onDelete = {
                    // Just for Example. Is not optimal!
                    myList = myList.toMutableList().also { it.remove(item) }
                },
                onEdit = { },
                modifier = Modifier.animateItemPlacement()
            ) {
                ListItem(headlineContent = { Text(text = "Headline text $item") },
                    supportingContent = { Text(text = "Supporting text $item") },
                    leadingContent = {
                        Icon(
                            imageVector = Icons.Outlined.AccountBox,
                            contentDescription = null
                        )
                    }
                )
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SwipeBox(
    modifier: Modifier = Modifier,
    onDelete: () -> Unit,
    onEdit: () -> Unit,
    content: @Composable () -> Unit
) {
    val swipeState = rememberSwipeToDismissBoxState()

    lateinit var icon: ImageVector
    lateinit var alignment: Alignment
    val color: Color

    when (swipeState.dismissDirection) {
        SwipeToDismissBoxValue.EndToStart -> {
            icon = Icons.Outlined.Delete
            alignment = Alignment.CenterEnd
            color = MaterialTheme.colorScheme.errorContainer
        }

        SwipeToDismissBoxValue.StartToEnd -> {
            icon = Icons.Outlined.Edit
            alignment = Alignment.CenterStart
            color =
                Color.Green.copy(alpha = 0.3f) // You can generate theme for successContainer in themeBuilder
        }

        SwipeToDismissBoxValue.Settled -> {
            icon = Icons.Outlined.Delete
            alignment = Alignment.CenterEnd
            color = MaterialTheme.colorScheme.errorContainer
        }
    }

    SwipeToDismissBox(
        modifier = modifier.animateContentSize(),
        state = swipeState,
        backgroundContent = {
            Box(
                contentAlignment = alignment,
                modifier = Modifier
                    .fillMaxSize()
                    .background(color)
            ) {
                Icon(
                    modifier = Modifier.minimumInteractiveComponentSize(),
                    imageVector = icon, contentDescription = null
                )
            }
        }
    ) {
        content()
    }

    when (swipeState.currentValue) {
        SwipeToDismissBoxValue.EndToStart -> {
            onDelete()
        }

        SwipeToDismissBoxValue.StartToEnd -> {
            LaunchedEffect(swipeState) {
                onEdit()
                swipeState.snapTo(SwipeToDismissBoxValue.Settled)
            }
        }

        SwipeToDismissBoxValue.Settled -> {
        }
    }
}
```