avatarElye

Summary

The article provides a comprehensive guide on implementing the SwipeToDismiss feature in Jetpack Compose, detailing the essentials, nice-to-have enhancements, and troubleshooting common issues.

Abstract

The article "Jetpack Compose Swipe To Dismiss Made Easy" by an author on Medium offers an in-depth tutorial on integrating the SwipeToDismiss functionality within Jetpack Compose. It starts by acknowledging the complexity of the topic despite the abundance of resources available. The author breaks down the implementation into minimal essentials, such as the SwipeToDismiss composable function and its required parameters, including the state, background, and dismissContent. The article also covers the definition of the SwipeToDismiss state, emphasizing the importance of handling state changes correctly to reflect updates in the underlying data. Beyond the basics, the author explores additional UI enhancements, such as animations and customizable swipe directions, and demonstrates how to manage the dismiss threshold for triggering actions. The article concludes by highlighting potential pitfalls and directs readers to further resources for creating a more discoverable and expressive SwipeToDismiss experience.

Opinions

  • The author believes that despite the availability of guides, understanding SwipeToDismiss in Jetpack Compose can still be challenging.
  • They suggest that the background parameter in SwipeToDismiss could be optional, implying a potential area for API improvement.
  • The author values the importance of animations in enhancing the user experience, advocating for their inclusion in the SwipeToDismiss implementation.
  • They highlight the flexibility of SwipeToDismiss by showing how to customize swipe directions and thresholds.
  • The author emphasizes the significance of the currentItem state being up-to-date to avoid bugs, indicating a common issue developers might face.
  • They share their personal experience of encountering several "gotchas" during implementation, suggesting that these lessons were learned through trial and error.
  • The author encourages readers to explore further enhancements and animations by referring to an additional article, indicating a commitment to sharing knowledge and promoting best practices in Jetpack Compose development.

Learning Android Development

Jetpack Compose Swipe To Dismiss Made Easy

Sharing Jetpack Compose Swipe To Dismiss the Simplest Possible Way

Photo by Vladimir Mun on Unsplash

There are many guides and links related to Jetpack Compose Swipe to Dismiss, but it still took me a few days to wrap my head around it.

You can get the code here https://github.com/elye/demo_android_jetpack_compose_list_update

Here I will share on

  1. The minimal essentials on SwipeToDismiss for Jetpack Compose
  2. The additional nice-to-have items make SwipeToDismiss better
  3. The few gotchas I encountered, where took me a few days to resolve.

The Minimal Essential of SwipeToDismiss

I believe in most cases, SwipeToDimiss is used together with LazyColumn. Without SwipeToDismiss, LazyColumn looks like the below, where it is the TodoItemRow is the UI of each row.

To have the ability to Swipe to Dismiss, we’ll wrap the TodoItemRow around with another SwipeToDismiss function.

As you can see above, there are 2 green boxes needed to get Swipe To Dismiss function properly. Let me illustrate them both below.

1. The SwipeToDismiss Composable Function

The function is still under experiment at the time of writing. The declaration looks as below.

@Composable
@ExperimentalMaterialApi
fun SwipeToDismiss(
    state: DismissState,
    modifier: Modifier = Modifier,
    directions: Set<DismissDirection> = setOf(EndToStart, StartToEnd),
    dismissThresholds: (DismissDirection) -> ThresholdConfig = { FractionalThreshold(0.5f) },
    background: @Composable RowScope.() -> Unit,
    dismissContent: @Composable RowScope.() -> Unit
)

From here we’ll know that only 3parameters are required

  1. state — this is needed to detect the state of SwipeToDelete, i.e. is it moving from Start to End or End to Start or Default (no movement)
  2. background — when we are swiping away the item, this will render how the exposed background looks. I personally think this can be made optional, with a blank body, but looks like it is needed.
  3. dismissContent — this is where we place the original Row item of the LazyColumn

2. The SwipeToDismiss State

This is the part where we define the state required for SwipeToDismiss.

The dismiss state If we don’t plan to have any action (e.g. remove the item after swipe), we can then simply define it as below.

val dismissState = rememberDismissState()

However, in our case, we wanted to remove the item if we detect a state change. We can then add confirmStateChange as below, where when any state changes, just remove the item.

val dismissState = rememberDismissState(
    confirmStateChange = {
        viewModel.removeRecord(currentItem)
        true
    }
)

The currentItem The other important control shown is, if the dismissState need to get to the latest item (outside confirmStatechange lambda), we should use the currentItem from rememberUpdatedState

val currentItem by rememberUpdatedState(item)

Else it might store the old item, and if the list is updated, some weird bug will exhibit, given the item is not the latest current one. More illustration is explained in the Gotcha section below (or you can also refer to this StackOverflow)

That’s it!! It’s not too hard to code the simple SwipeToDismiss

But it is not as nice, on some UI aspects. Let’s explore a little nicer UI consideration below

The Nice-To-Have of SwipeToDismiss

The animation

When we swipe an item out, if using the most basic implementation, there’s no animation. The item below quickly displays up

If we like some animation, we can add Modifier to SwipeToDimiss as shown below.

                    SwipeToDismiss(
                        state = ...,
                        modifier = Modifier
                            .padding(vertical = 1.dp)
                            .animateItemPlacement(),
                        background = ...,
                        dismissContent = ...
                    )

The padding is just to add space in between the item

The animateItemPlacement is the one that will enable some animation moving the items below up

The supported direction

We can either swipe Right (aka End) or Left (aka Start) through the direction parameter of SwipeToDismiss.

By default, both directions are enabled. We can override to switch off one of them.

directions: Set<DismissDirection> = setOf(EndToStart, StartToEnd),
  • The EndToStart is enabling swipe from right to left
  • The StartToEnd is enabling swipe from left to right

We can also customize the behavior for different actions, in the confirmStatechange lambda of the dismissState variable.

val dismissState = rememberDismissState(
    confirmStateChange = {
        when (it) {
            DismissValue.DismissedToEnd -> {
                // Do Something when swipe Start To End
                true
            }
            DismissValue.DismissedToStart -> {
                // Do Something when swipe End To Start
                true
            }
            else -> { false }
        }
    }
)

The dismiss threshold

When we swipe, we only want the action to be triggered after we swipe across a certain threshold e.g. 50%.

This can be set through dismissThresholds parameter of SwipeToDismiss. By default, it is a 50% threshold (for either direction).

dismissThresholds: (DismissDirection) -> ThresholdConfig = 
    { FractionalThreshold(0.5f) }

We can set a different value for different directions as shown below, where I set it for

  • StartToEnd, it will get triggered when it moves 66%
  • EndToStart, it will get triggered when it moves 50%
SwipeToDismiss(
    state = ...,
    directions = ...,
    dismissThresholds = { direction ->
        FractionalThreshold(
           if (direction == DismissDirection.StartToEnd) 0.66f else 0.50f)
    },
    background = ...,
    dismissContent = ...
)

To demonstrate that, I have made changes to show that it has been enabled when the background is changed from Grey to either Red or Green (which is done with the Background setting change as per the next point)

Background Change With DismissState value

As you probably see in the above GIF, that

When we move StartToEnd - There’s a Checked Icon on the far left - The background turns from Grey to Green as it gets enabled - The Checked Icon grows bigger as it gets enabled

When we move EndToStart - There’s a Bin Icon on the far right - The background turns from Grey to Red as it gets enabled - The Bin Icon grows bigger as it gets enabled

We have all this controlled through the Background parameter of SwipeToDismiss by passing in our dismissState as shown in the code below

The SwipeBackground shown below is just a custom Composable function I made to factorize the code. It is not part of SwipeToDismiss requirement.

background = {
    SwipeBackground(dismissState)
}

With the dismissState, we can gather

  • dismissState.dismissDirection where it is either StartToEnd or EndToStart (or null i.e. the default)
  • dismissState.targetValue where it is either Defaul (when the dismiss is not yet triggered) or tDismissedToEnd or DismissedToStart

The complete code for SwipeBackground is as below, where it is relatively self-explanatory of the Background UI drawn

@Composable
@OptIn(ExperimentalMaterialApi::class)
fun SwipeBackground(dismissState: DismissState) {
    val direction = dismissState.dismissDirection ?: return

    val color by animateColorAsState(
        when (dismissState.targetValue) {
            DismissValue.Default -> Color.LightGray
            DismissValue.DismissedToEnd -> Color.Green
            DismissValue.DismissedToStart -> Color.Red
        }
    )
    val alignment = when (direction) {
        DismissDirection.StartToEnd -> Alignment.CenterStart
        DismissDirection.EndToStart -> Alignment.CenterEnd
    }
    val icon = when (direction) {
        DismissDirection.StartToEnd -> Icons.Default.Done
        DismissDirection.EndToStart -> Icons.Default.Delete
    }
    val scale by animateFloatAsState(
        if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f
    )

    Box(
        Modifier
            .fillMaxSize()
            .background(color)
            .padding(horizontal = 20.dp),
        contentAlignment = alignment
    ) {
        Icon(
            icon,
            contentDescription = "Localized description",
            modifier = Modifier.scale(scale)
        )
    }
}

Nicer UI and animation

If you like to explore nicer UI or animation related to SwipeToDismiss, e.g. make it more discoverable, below is another article with sample code that will help.

The SwipeToDimiss Gotchas to Avoid

During learning, I encountered a few tricky bits, and worth sharing with you, so you don’t learn the hard way.

As this writing seems to get a little lengthy, let me share it in another blog.

Android App Development
Mobile App Development
App Development
Jetpack Compose
AndroidDev
Recommended from ReadMedium
avatarNine Pages Of My Life
Flow Layouts in Jetpack Compose

🎯Index

4 min read