
SwipeTo explore different implementations in Jetpack Compose
SwipeTo explore different implementations in Jetpack Compose 관련

Swipe gesturesprovide a natural way to interact with elements in an app, adding intuitive controls for actions like dismissing items or revealing options.Jetpack Composemakes it easy to implement in various ways. With recent updates of the Compose libraries, new APIs make swipe-based interactions simpler and more maintainable.
In this article, we’ll explore how to implement theSwipeToDismissandSwipeToRevealfunctionality and customize them for various use cases, empowering you to create dynamic, responsive UIs.
Base Implementation withdetectHorizontalDragGestures
The first approach for implementing swipe-based interactions is to usedetectHorizontalDragGestures
, a flexible and foundational solution that allows for full customization. This method enables bothSwipeToDismiss
andSwipeToReveal
functionalities by managing the horizontal drag manually. Below is an example of how to implement this in a composable:
@Composable
fun LibraryBook(
onClickRead: () -> Unit,
onClickDelete: () -> Unit
) {
var offsetX by remember { mutableFloatStateOf(0f) }
Box(
modifier
.fillMaxSize()
.pointerInput(Unit) {
detectHorizontalDragGestures { \_, dragAmount ->
offsetX = (offsetX + dragAmount).coerceIn(-300f, 0f)
}
}
) {
// Actions revealed
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onClickDelete) {
Icon(Icons.Default.Delete, contentDescription = "")
}
}
// Main content
Box(
modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), 0) }
) {
InternalLibraryBook()
}
}
}

In this implementation:
- We maintain an
offsetX
state to control the horizontal position of the item as it’s dragged. DetectHorizontalDragGestures
handles horizontal dragging, updatingoffsetX
within a specified range to prevent excessive movement.- The main content is shifted based on
offsetX
, revealing the delete action as you swipe.
This approach is straightforward, but it provides the flexibility to expand and customize as needed. If you want to dive deeper into this solution,Philipp Lackner’s videoprovides an excellent walkthrough. Philipp shares various Compose techniques in his videos, so consider following him for more useful tips and tutorials.
Implementation with SwipeToDismissBox
With recent updates to theCompose libraries, we now have theSwipeToDismissBox
, which provides a more structured and controllable approach to swipe-based interactions. This component simplifies the process of implementing dismiss gestures and offers better control over the swipe state. Here’s how it enhances the previous implementation:
@Composable
fun LibraryBook2(
modifier: Modifier = Modifier,
onClickRead: () -> Unit,
onClickDelete: () -> Unit
) {
val dismissState = rememberSwipeToDismissBoxState(confirmValueChange = {
when (it) {
SwipeToDismissBoxValue.EndToStart -> {
onClickDelete()
true
}
SwipeToDismissBoxValue.StartToEnd -> {
onClickRead()
true
}
else -> false
}
})
SwipeToDismissBox(
modifier = modifier,
state = dismissState,
backgroundContent = {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Read action on swipe from start to end
AnimatedVisibility(
visible = dismissState.targetValue == SwipeToDismissBoxValue.StartToEnd,
enter = fadeIn()
) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.MenuBook,
contentDescription = "Read"
)
}
Spacer(modifier = Modifier.weight(1f))
// Delete action on swipe from end to start
AnimatedVisibility(
visible = dismissState.targetValue == SwipeToDismissBoxValue.EndToStart,
enter = fadeIn()
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete"
)
}
}
}
) {
InternalLibraryBook()
}
}
In this updated example:
SwipeToDismissBox
manages the swipe state internally, which simplifies the swipe handling compared to thedetectHorizontalDragGestures
approach.- The
backgroundContent
is displayed conditionally based on the swipe direction, using in my case,AnimatedVisibility
to smoothly show icons for delete and read actions.
Resetting the Swipe Position
To reset the swipe position after an action is taken, you can leverageLaunchedEffect
to monitordismissState.currentValue
and trigger a reset when a swipe is completed:
val dismissState = rememberSwipeToDismissBoxState()
LaunchedEffect(dismissState.currentValue) {
when (dismissState.currentValue) {
SwipeToDismissBoxValue.EndToStart -> {
onClickDelete()
dismissState.reset()
}
SwipeToDismissBoxValue.StartToEnd -> {
onClickRead()
dismissState.reset()
}
else -> { /\* No action needed \*/ }
}
}

Implementing SwipeToReveal with anchoredDraggable
TheSwipeToDismissBox
works well for swipe to dismiss interactions, but if we want to implementSwipeToReveal
(where swiping reveals options rather than dismissing the item) we need a different approach. I found a powerful alternative with theanchoredDraggable
API, as it allows us to define anchor points where specific actions can be triggered, making it ideal for reveal-based interactions.
Here’s the example of implementingSwipeToReveal
withanchoredDraggable
:
enum class SwipeToRevealValue { Read, Resting, Delete }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LibraryBook3(
onClickRead: () -> Unit,
onClickDelete: () -> Unit
) {
val density = LocalDensity.current
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val dragState = remember {
val actionOffset = with(density) { 100.dp.toPx() }
AnchoredDraggableState(
initialValue = SwipeToRevealValue.Resting,
anchors = DraggableAnchors {
SwipeToRevealValue.Read at -actionOffset
SwipeToRevealValue.Resting at 0f
SwipeToRevealValue.Delete at actionOffset
},
positionalThreshold = { distance -> distance \* 0.5f },
velocityThreshold = { with(density) { 100.dp.toPx() } },
snapAnimationSpec = tween(),
decayAnimationSpec = decayAnimationSpec,
)
}
val overScrollEffect = ScrollableDefaults.overscrollEffect()
Box(
modifier = Modifier.fillMaxSize()
) {
// Main content that moves with the swipe
Box(
modifier = Modifier
.anchoredDraggable(
dragState,
Orientation.Horizontal,
overscrollEffect = overScrollEffect
)
.overscroll(overScrollEffect)
.offset {
IntOffset(
x = dragState.requireOffset().roundToInt(),
y = 0
)
}
) {
InternalLibraryBook()
}
// actions for "Read" and "Delete"
Row(
modifier = Modifier.matchParentSize(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Read Action
AnimatedVisibility(
visible = dragState.currentValue == SwipeToRevealValue.Read,
enter = slideInHorizontally(animationSpec = tween()) { it },
exit = slideOutHorizontally(animationSpec = tween()) { it }
) {
IconButton(onClick = onClickRead) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.MenuBook,
contentDescription = "Read"
)
}
}
Spacer(modifier = Modifier.weight(1f))
// Delete Action
AnimatedVisibility(
visible = dragState.currentValue == SwipeToRevealValue.Delete,
enter = slideInHorizontally(animationSpec = tween()) { -it },
exit = slideOutHorizontally(animationSpec = tween()) { -it }
) {
IconButton(onClick = onClickDelete) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete"
)
}
}
}
}
}

In this setup:
AnchoredDraggableState
allows us to set specific anchor points for different actions. Here, swiping left reveals the delete option, while swiping right reveals the read option.AnimatedVisibility
andslideInHorizontally
are used to animate the icons as they are revealed or hidden, creating a smooth interaction.
This approach work well also in the case of the swipe to dismiss interactions. In this case we need to add aLaunchedEffect
to call our callbacks at the right moment:
LaunchedEffect(dragState) {
snapshotFlow { dragState.settledValue }
.collectLatest {
when (it) {
SwipeToRevealValue.Read -> onClickRead()
SwipeToRevealValue.Delete -> onClickDelete()
else -> {}
}
delay(30)
dragState.animateTo(SwipeToRevealValue.Resting)
}
}

TheLaunchedEffect
triggers the appropriate action based on the settled value, then resets the swipe position to maintaina clean UI state after each swipe.
Conclusion
In this article, we’ve explored three powerful approaches to implementing swipe-based interactions in Jetpack Compose:detectHorizontalDragGestures
,SwipeToDismissBox
, andanchoredDraggable
.
Each method has its strengths, allowing for a range of customization and control over swipe behaviors.
detectHorizontalDragGestures
provides a low-level, customizable approach, ideal if you need control over gesture handling.SwipeToDismissBox
simplifies the setup for dismissible items with built-in state management, making it a great choice for straightforward swipe-to-dismiss interactions.anchoredDraggable
offers precise control over anchored states, making it well-suited for swipe functionalities.
By choosing the right tool for the job, you can create smooth, intuitive swipe interactions that enhance your app’s UX.Compose continues to evolve, and with these options, you can build flexible and engaging interfaces that feel natural and responsive to users.
If you found this article interesting, feel free tofollow mefor more insightful content on Android development and Jetpack Compose. I publish new articles almost every week. Don’t hesitate to share your comments or reach out to me onLinkedIn (stefano-natali-q21
) if you prefer.
Have a great day!
Info
This article is previously published on proandroiddev
)
