KMPSwipe
1.0.2indexedEnhances user interfaces by integrating swipe gestures for interactive functionality in Compose-based applications, enabling dynamic actions on custom UI components with intuitive user experiences.
Enhances user interfaces by integrating swipe gestures for interactive functionality in Compose-based applications, enabling dynamic actions on custom UI components with intuitive user experiences.
KMPSwipe is a complete Kotlin Cross-Platform library designed to integrate swipe gestures into your Compose-based applications, targeting both Android and iOS platforms with a unified API. This library allows developers to add dynamic and interactive swipe functionalities to any composable component, be it a Card, Box or any custom UI element, enhancing the user experience with intuitive gestures.
| Android | iOS |
|---|---|
![]() | ![]() |
Gradle (Kotlin DSL)
dependencies{
implementation("io.github.ismoy:kmpswipe:1.0.0")// use latest version
}
Gradle (Kotlin DSL)
commonMain.dependencies {
implementation("io.github.ismoy:kmpswipe:1.0.0") // use latest version
}
KMPSwipe is based on four fundamental concepts: SwipeDirection: Defines the direction of the swipe (Left, Right, None) SwipeState: Defines the current state of the swipe (Start, Swiping, End, Cancelled) SwipeableContent: The UI component that will be swiped SwipeBackgrounds: The UI components that are displayed underneath during the swipe
KmpSwipe(
onSwipeComplete = { direction ->
when (direction) {
SwipeDirection.Left -> { /* Left action */ }
SwipeDirection.Right -> { /* Right action */ }
else -> {}
}
}
) { _, _ ->
// Your slide content here
Box(modifier = Modifier.height(160.dp)
.fillMaxSize()
.padding(horizontal = 16.dp, vertical = 20.dp)
.background(color = Color.LightGray, shape = RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center) {
Text("Slide this item left or right")
}
}
enum class SwipeDirection {
None,
Left,
Right
}
enum class SwipeState {
Start, // Beginning of the gesture
Swiping, // During the slide
End, // Slide completed
Cancelled // Slide cancelled
The main component that wraps your content to make it slideable.
// In your Screen o App.kt
val itemList = listOf(
Item(id = "1", title = "Item 1", description = "Descripción del Item 1"),
Item(id = "2", title = "Item 2", description = "Descripción del Item 2"),
Item(id = "3", title = "Item 3", description = "Descripción del Item 3")
)
SwipeableList(items = itemList)
@Composable
fun ItemCard(item: Item) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = 2.dp
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(text = item.title, style = MaterialTheme.typography.h6)
Text(text = item.description, style = MaterialTheme.typography.body2)
}
}
}
}
data class Item(
val id: String,
val title: String,
val description: String,
)
// In your Screen o App.kt
val emailList = listOf(
Email(id = "1", sender = "Alice", time = "10:30 AM", subject = "Important Meeting", preview = "Hello, don't forget today's meeting at 3 PM..."),
Email(id = "2", sender = "Bob", time = , subject = , preview = ),
Email(id = , sender = , time = , subject = , preview = )
)
EmailInbox(emails = emailList)
fun {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = (swipeState == SwipeState.Swiping) dp dp,
) {
Column(
modifier = Modifier.padding(dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = email.sender,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.subtitle1
)
Text(
text = email.time,
color = Color.Gray,
style = MaterialTheme.typography.caption
)
}
Spacer(modifier = Modifier.height(dp))
Text(
text = email.subject,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.body1
)
Spacer(modifier = Modifier.height(dp))
Text(
text = email.preview,
maxLines = ,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body2
)
}
}
}
data class Email(
val id: String,
val sender: String,
val time: String,
val subject: String,
val preview: String
)
Dynamic slip threshold
KmpSwipe(
// ...
dynamicSwipeThreshold = { threshold ->
// Dynamically calculate the threshold based on some logic
// For example, increase the threshold based on content
if (isLongContent) threshold * 1.5f else threshold
}
) { swipeState, swipeDirection ->
// Your content
}
KmpSwipe(
// ...
swipeThreshold = 120.dp, // Distance to complete the slide
resistance = 1.5f, // Greater resistance = more difficult movement
springStiffness = 700f, // Higher stiffness = faster animation
swipeLimitMultiplier = 2f, // Maximum slip limit
dampingRatio = Spring.DampingRatioMediumBouncy, // Bounce Type
vibrationEnabled = true, // Haptic feedback upon completion
) { swipeState, swipeDirection ->
// Your content
}
KmpSwipe(
// ...
swipeDirections = setOf(SwipeDirection.Left), // Only allow left swipe
) { swipeState, swipeDirection ->
// Your content
}
KmpSwipe(
// ...
swipeDirections = setOf(SwipeDirection.Right), // Only allow right swipe
) { swipeState, swipeDirection ->
// Your content
}
KmpSwipe(
// ...
onSwipeStateChange = { state ->
// Track the state to perform further actions
}
) { swipeState, swipeDirection ->
val backgroundColor = when (swipeState) {
SwipeState.Start -> Color.White
SwipeState.Swiping -> if (swipeDirection == SwipeDirection.Left)
Color.Red.copy(alpha = 0.1f)
else
Color.Green.copy(alpha = 0.1f)
SwipeState.End -> Color.LightGray
SwipeState.Cancelled -> Color.White
}
Card(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor),
elevation = (swipeState == SwipeState.Swiping) dp dp
) {
}
}
KmpSwipe(
// ...
leftBackground = { offset ->
progress = (offset / dp).coerceIn(, )
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red.copy(alpha = progress), RoundedCornerShape(dp)),
contentAlignment = Alignment.CenterEnd
) {
Row(
modifier = Modifier.padding(end = dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = ,
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = ( + progress * ).sp
)
Spacer(modifier = Modifier.width(dp))
Icon(
imageVector = Icons.Default.Delete,
contentDescription = ,
tint = Color.White,
modifier = Modifier.size(( + progress * ).dp)
)
}
}
}
) { swipeState, swipeDirection ->
}
KMPSwipe is designed to be highly performant, but here are some techniques to optimize it further:
LazyColumn {
items(items, key = { it.id }) { item ->
KmpSwipe(
// ...
) { swipeState, swipeDirection ->
// Content
}
}
}
// Extract static components
val leftBackground: @Composable (Dp) -> Unit = { offset ->
// Left background implementation
}
val rightBackground: @Composable (Dp) -> Unit = { offset ->
// Right background implementation
}
// Then use them in KmpSwipe
KmpSwipe(
// ...
leftBackground = leftBackground,
rightBackground = rightBackground
) { swipeState, swipeDirection ->
// Content
}
// Avoid creating new lambda functions on each recomposition
val onSwipeComplete = remember<(SwipeDirection) -> Unit> { { direction ->
// Actions upon completion of swipe
} }
KmpSwipe(
// ...
onSwipeComplete = onSwipeComplete
) { swipeState, swipeDirection ->
// Content
}
Task list with swipe
val taskItemList = listOf(
Task(id = "1", title = "Buy groceries", description = "Milk, eggs, bread, and fruits", isCompleted = false, dueDate = "2025-02-26", isOverdue = ),
Task(id = , title = , description = , isCompleted = , dueDate = , isOverdue = ),
Task(id = , title = , description = , isCompleted = , dueDate = , isOverdue = )
)
TaskList(taskItemList)
data class Task(
val id: String,
val title: String,
val description: String,
val isCompleted: Boolean,
val dueDate: String?,
val isOverdue: Boolean
)
KmpSwipe
You can use different thresholds for different actions:
KmpSwipe(
// ...
onSwipe = { direction, offset ->
if (direction == SwipeDirection.Left && offset.value > 150f) {
// Additional action when sliding beyond a certain point
}
}
) { swipeState, swipeDirection ->
// Content
}
You can persist state with rememberSaveable:
val persistedState = rememberSaveable { mutableStateOf(SwipeState.Start) }
val persistedDirection = rememberSaveable { mutableStateOf(SwipeDirection.None) }
KmpSwipe(
// ...
onSwipeStateChange = { state ->
persistedState.value = state
},
onSwipeComplete = { direction ->
persistedDirection.value = direction
// Other actions
}
) { swipeState, swipeDirection ->
// Use persistedState.value and persistedDirection.value if necessary
// to maintain state across recompositions
}
The return animation uses the springStiffness and dampingRatio parameters:
KmpSwipe(
// ...
springStiffness = 800f, // Faster
dampingRatio = Spring.DampingRatioNoBouncy, // No bounce
) { swipeState, swipeDirection ->
// Content
}
Use the enabled parameter:
KmpSwipe(
// ...
enabled = !isLoading && itemIsSwipeable,
) { swipeState, swipeDirection ->
// Content
}
Use the onSwipeVelocity callback:
KmpSwipe(
// ...
onSwipeVelocity = { velocity ->
if (abs(velocity) > 1000f) {
//Fast slide, you could make a special animation
}
}
) { swipeState, swipeDirection ->
// Content
}
KMPSwipe is an open source project and contributions are welcome. To contribute:
KMPSwipe is licensed under the MIT license. See the LICENSE file for more details.
KmpSwipe(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
onSwipeComplete = { direction ->
when (direction) {
SwipeDirection.Left -> { /* Delete element */ }
SwipeDirection.Right -> { /* Archive element */ }
else -> {}
}
},
leftBackground = { _ ->
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red, RoundedCornerShape(8.dp)),
contentAlignment = Alignment.CenterEnd
) {
Text(
text = "Delete",
color = Color.White,
modifier = Modifier.padding(end = 32.dp)
)
}
},
rightBackground = { _ ->
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Green, RoundedCornerShape(8.dp)),
contentAlignment = Alignment.CenterStart
) {
Text(
text = "Archive",
color = Color.Black,
modifier = Modifier.padding(start = 32.dp)
)
}
}
) { _, _ ->
Card(
modifier = Modifier.fillMaxWidth(),
elevation = 4.dp
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = "Basic sliding element", style = MaterialTheme.typography.h6)
Text(text = "Swipe me", style = MaterialTheme.typography.body1)
}
}
}
fun SwipeableList(items: List<Item>) {
LazyColumn {
items(items, key = { it.id }) { item ->
KmpSwipe(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 8.dp),
onSwipeComplete = { direction ->
when (direction) {
SwipeDirection.Left -> { /* Delete */ }
SwipeDirection.Right -> { /* Archive */ }
else -> {}
}
},
leftBackground = { _ ->
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red, RoundedCornerShape(8.dp)),
contentAlignment = Alignment.CenterEnd
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = Color.White,
modifier = Modifier.padding(end = 32.dp)
)
Text(
text = "Delete",
color = Color.White,
modifier = Modifier.padding(end = 90.dp)
)
}
},
rightBackground = { _ ->
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Blue, RoundedCornerShape(8.dp)),
contentAlignment = Alignment.CenterStart
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Archive",
tint = Color.White,
modifier = Modifier.padding(start = 32.dp)
)
Text(
text = "Archive",
color = Color.White,
modifier = Modifier.padding(start = 90.dp)
)
}
}
) { _, _ ->
ItemCard(item)
}
}
}
}
fun EmailInbox(emails: List<Email>) {
LazyColumn {
items(emails, key = { it.id }) { email ->
KmpSwipe(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 4.dp),
onSwipeComplete = { direction ->
when (direction) {
SwipeDirection.Left -> { /* Delete email */ }
SwipeDirection.Right -> { /* Archive email */ }
else -> {}
}
},
onSwipeStateChange = { state ->
// Status monitoring for analysis or debugging
},
swipeThreshold = 120.dp, // Custom threshold
resistance = 1.2f, // Greater resistance
leftBackground = { _ ->
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red, RoundedCornerShape(4.dp)),
contentAlignment = Alignment.CenterEnd
) {
Row(
modifier = Modifier.padding(end = 24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Delete",
color = Color.White,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = Color.White
)
}
}
},
rightBackground = { offset ->
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Gray, RoundedCornerShape(4.dp)),
contentAlignment = Alignment.CenterStart
) {
Row(
modifier = Modifier.padding(start = 24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Archive",
tint = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Archive",
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
}
) { swipeState, swipeDirection ->
EmailItem(email, swipeState)
}
}
}
}
fun TaskList(tasks: List<Task>) {
LazyColumn {
items(tasks, key = { it.id }) { task ->
KmpSwipe(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
onSwipeComplete = { direction ->
when (direction) {
SwipeDirection.Left -> { /* Delete Task */ }
SwipeDirection.Right -> { /* Add Task */ }
else -> {}
}
},
leftBackground = { offset ->
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red.copy(alpha = 0.8f), RoundedCornerShape(4.dp)),
contentAlignment = Alignment.CenterEnd
) {
Text(
text = "Delete",
color = Color.White,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(end = 24.dp)
)
}
},
rightBackground = { offset ->
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF4CAF50), RoundedCornerShape(4.dp)),
contentAlignment = Alignment.CenterStart
) {
Text(
text = "Add",
color = Color.White,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 24.dp)
)
}
}
) { swipeState, completedDirection ->
TaskItem(task,swipeState,completedDirection)
}
}
}
}
fun TaskItem(task: Task, swipeState: SwipeState, completedDirection: SwipeDirection) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = 1.dp,
shape = RoundedCornerShape(4.dp),
backgroundColor = if (task.isCompleted) Color(0xFFE8F5E9) else Color.White
) {
Column(modifier = Modifier.padding(16.dp)) {
val chipText = when (swipeState) {
SwipeState.Swiping -> if (completedDirection == SwipeDirection.Right) "Adding" else "Deleting"
SwipeState.End -> if (completedDirection == SwipeDirection.Left) "Deleted" else "Added"
else -> ""
}
if (chipText.isNotEmpty()) {
Chip(
onClick = {},
modifier = Modifier.padding(bottom = 8.dp)
) {
Text(text = chipText)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = task.title,
style = MaterialTheme.typography.subtitle1,
textDecoration = if (task.isCompleted) TextDecoration.LineThrough else TextDecoration.None
)
if (task.description.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = task.description,
style = MaterialTheme.typography.body2,
color = Color.Gray,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
if (task.dueDate != null) {
Text(
text = task.dueDate,
style = MaterialTheme.typography.caption,
color = if (task.isOverdue) Color.Red else Color.Gray
)
}
}
}
}
}
| Parameter | Type | Description | default value |
|---|
modifier | Modifier | Compose Modifier for the Container | Modifier |
onSwipeComplete | (SwipeDirection) -> Unit | Callback when a swipe is completed | {} |
onSwipe | (SwipeDirection, Dp) -> Unit | Callback during sliding | { _, _ -> } |
onSwipeStateChange | (SwipeState) -> Unit | Callback when state changes | {} |
swipeThresholdDp | Dp | Minimum distance to complete a slide | 100.dp |
resistance | Float | Sliding resistance factor | 1f |
springStiffness | Float | Spring effect stiffness in animation | 500f |
swipeLimitMultiplier | Float | Multiplier for the slip limit | 1.5f |
backgroundPaddingHorizontal | Dp | Horizontal padding for backgrounds | 6.dp |
vibrationEnabled | Boolean | Enable haptic feedback | true |
dampingRatio | Float | Damping ratio for animations | Spring.DampingRatioMediumBouncy |
leftBackground | @Composable (offset: Dp) -> Unit | Composable for left slide background | {} |
rightBackground | @Composable (offset: Dp) -> Unit | Composable for right slide background | {} |
enabled | Boolean | Enable/disable swipe gestures | true |
swipeDirections | Set<SwipeDirection> | Allowed directions for swiping | setOf(Left, Right) |
onSwipeVelocity | (Float) -> Unit | Callback with the speed of the slide | {} |
dynamicSwipeThreshold | ((Dp) -> Dp)? | Function to dynamically calculate the threshold | null |
content | @Composable (SwipeState, SwipeDirection) -> Unit | Sliding content | - |
Surfaced from shared tags and platforms — no rankings paid for.