KMPSwipe
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.
Índice
- How to use
- Basic concepts
- Main components
- Examples of use
- Advanced customization
- Performance optimization
- Integration with lists
- Full API
- FAQ
| Android | iOS |
|---|---|
![]() |
![]() |
How use
Native Android
Gradle (Kotlin DSL)
dependencies{
implementation("io.github.ismoy:kmpswipe:1.0.0")// use latest version
}
KMP (Kotlin multiplatform)
Gradle (Kotlin DSL)
commonMain.dependencies {
implementation("io.github.ismoy:kmpswipe:1.0.0") // use latest version
}
Basic concepts
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
Minimal example
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")
}
}
Main components
SwipeDirection
enum class SwipeDirection {
None,
Left,
Right
}
SwipeState
enum class SwipeState {
Start, // Beginning of the gesture
Swiping, // During the slide
End, // Slide completed
Cancelled // Slide cancelled
KmpSwipe
The main component that wraps your content to make it slideable.
Examples of use
Basic sliding element
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)
}
}
}
List with slideable elements
// 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 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)
}
}
}
}
@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,
)
Swipe-in inbox interface
// 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 = "11:15 AM", subject = "Project Update", preview = "I'm sending you the latest changes in the project report..."),
Email(id = "3", sender = "Charlie", time = "12:00 PM", subject = "Event Invitation", preview = "You are invited to the networking event this Friday...")
)
EmailInbox(emails = emailList)
@Composable
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 EmailItem(email: Email, swipeState: SwipeState) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = if (swipeState == SwipeState.Swiping) 8.dp else 2.dp,
// Animación de elevación durante el deslizamiento
) {
Column(
modifier = Modifier.padding(16.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(4.dp))
Text(
text = email.subject,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.body1
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = email.preview,
maxLines = 2,
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
)
## Advanced customization 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
}
Customizing Swipe Behavior
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
}
Control of allowed swipe
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
}
Visual change based on state
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 = if (swipeState == SwipeState.Swiping) 8.dp else 2.dp
) {
// Your Content
}
}
Advanced backgrounds with animation
KmpSwipe(
// ...
leftBackground = { offset ->
val progress = (offset / 100.dp).coerceIn(0f, 1f)
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red.copy(alpha = progress), RoundedCornerShape(8.dp)),
contentAlignment = Alignment.CenterEnd
) {
Row(
modifier = Modifier.padding(end = 24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Delete",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = (16 + progress * 4).sp // Text that grows with sliding
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = Color.White,
modifier = Modifier.size((24 + progress * 8).dp) // Growing icon
)
}
}
}
) { swipeState, swipeDirection ->
// Your content
}
Performance optimization
KMPSwipe is designed to be highly performant, but here are some techniques to optimize it further:
- Using keys on list items
kotlin LazyColumn { items(items, key = { it.id }) { item -> KmpSwipe( // ... ) { swipeState, swipeDirection -> // Content } } } - Avoid unnecessary recompositions ```kotlin // 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 }
3. Optimize callbacks
```kotlin
// 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
}
Integration with lists
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 = false),
Task(id = "2", title = "Finish project report", description = "Complete the final draft and send it to the manager", isCompleted = false, dueDate = "2025-02-27", isOverdue = false),
Task(id = "3", title = "Doctor's appointment", description = "Annual check-up at 10:00 AM", isCompleted = false, dueDate = "2025-02-28", isOverdue = false)
)
TaskList(taskItemList)
@Composable
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)
}
}
}
}
@Composable
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
)
}
}
}
}
}
data class Task(
val id: String,
val title: String,
val description: String,
val isCompleted: Boolean,
val dueDate: String?,
val isOverdue: Boolean
)
Full API
KmpSwipe
| 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 | - |
FAQ
How can I handle multiple actions in one swipe?
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
}
How can I save the sliding state?
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
}
Is it possible to customize the return animation?
The return animation uses the springStiffness and dampingRatio parameters:
KmpSwipe(
// ...
springStiffness = 800f, // Faster
dampingRatio = Spring.DampingRatioNoBouncy, // No bounce
) { swipeState, swipeDirection ->
// Content
}
How can I disable swiping conditionally?
Use the enabled parameter:
KmpSwipe(
// ...
enabled = !isLoading && itemIsSwipeable,
) { swipeState, swipeDirection ->
// Content
}
How can I get the sliding speed?
Use the onSwipeVelocity callback:
KmpSwipe(
// ...
onSwipeVelocity = { velocity ->
if (abs(velocity) > 1000f) {
//Fast slide, you could make a special animation
}
}
) { swipeState, swipeDirection ->
// Content
}
Contribution
KMPSwipe is an open source project and contributions are welcome. To contribute:
- Fork the repository
- Create a branch for your feature (git checkout -b feature/amazing-feature)
- Commit your changes (git commit -m 'Add some amazing feature')
- Push to branch (git push origin feature/amazing-feature)
- Open a Pull Request
Native iOS Support
Good news! Native iOS support is on the way. We're working hard to bring the functionality of KmpSwipe to the iOS platform, allowing you to use the same swiping logic in your iOS applications. Stay tuned for future updates and announcements regarding the availability of iOS support.
License
KMPSwipe is licensed under the MIT license. See the LICENSE file for more details.
Built With
- kotlin


Log in or sign up for Devpost to join the conversation.