Discovering Material 3 for Android
This series of articles is designed to help you explore the latest addition to Material 3 for Android. In this series, we will take a closer look at the different components and features of Material 3, and explore how they can be used to create modern and user-friendly Android apps.
Contents
The iOS Swipe-to-Delete Gesture
I’ve recently engaged in an interesting conversation with an iOS engineer about the “swipe to delete” gesture, a simple way in iOS to remove items from a list. While this gesture might be ‘invisible’ on the screen, it is a well-known pattern among iOS users. Contrarily, in the Android world, this gesture has never been very intuitive.
More than ever, Material 3 must offer components that can be used on platforms other than those of Google. Compose Multiplatform and any platform if you are creating a design system need this.
SwipeToDismissBox: Show Me the Code
At its core, SwipeToDismissBox allows elements within an app to be dismissed with a simple swipe gesture. This pattern, familiar to iOS users, brings a nice hidden feature to your power users in any Android application.
Truth is, it’s a bit more generic than “swipe to dismiss”. You can perform two kinds of actions: swipe from start to end and swipe from end to start.
This component makes sense within a List only.
The Google Material team ensures new components are open to customization. The swipe and color management could just as well have been prepared in the component, but Google prefers to give developers more flexibility. Therefore, to offer a component similar to what we find on iOS, we can encapsulate the SwipeToDismissBox like this:
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun SwipeToDismissListItem(
modifier: Modifier = Modifier,
onEndToStart: () -> Unit = {},
onStartToEnd: () -> Unit = {},
content: @Composable () -> Unit
) {
// 1. State is hoisted here
val dismissState = rememberSwipeToDismissBoxState()
SwipeToDismissBox(
modifier = modifier,
state = dismissState,
backgroundContent = {
// 2. Animate the swipe by changing the color
val color by animateColorAsState(
targetValue = when (dismissState.targetValue) {
SwipeToDismissBoxValue.Settled -> Color.LightGray
SwipeToDismissBoxValue.StartToEnd -> Color.Green
SwipeToDismissBoxValue.EndToStart -> Color.Red
},
label = "swipe"
)
Box(
modifier = Modifier
.fillMaxSize()
.background(color) // 3. Set the animated color here
) {
// 4. Show the correct icon
when (dismissState.targetValue) {
SwipeToDismissBoxValue.StartToEnd -> {
Icon(
modifier = Modifier
.align(androidx.compose.ui.Alignment.CenterStart)
.padding(start = 16.dp),
imageVector = androidx.compose.material.icons.Icons.Default.Edit,
contentDescription = "edit"
)
}
SwipeToDismissBoxValue.EndToStart -> {
Icon(
modifier = Modifier
.align(androidx.compose.ui.Alignment.CenterEnd)
.padding(end = 16.dp),
imageVector = androidx.compose.material.icons.Icons.Default.Delete,
contentDescription = "delete"
)
}
SwipeToDismissBoxValue.Settled -> {
// Nothing to do
}
}
}
}
) {
content()
}
// 5. Trigger the callbacks
when (dismissState.currentValue) {
SwipeToDismissBoxValue.EndToStart -> {
LaunchedEffect(dismissState.currentValue) {
onEndToStart()
// 6. Don't forget to reset the state value
dismissState.snapTo(SwipeToDismissBoxValue.Settled)
}
}
SwipeToDismissBoxValue.StartToEnd -> {
LaunchedEffect(dismissState.currentValue) {
onStartToEnd()
dismissState.snapTo(SwipeToDismissBoxValue.Settled)
}
}
SwipeToDismissBoxValue.Settled -> {
// Nothing to do
}
}
}
Using in a LazyColumn
Finally, we put our SwipeToDismissListItem within a LazyColumn:
LazyColumn(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
) {
itemsIndexed(
key = { index, _ -> index },
items = myList
) { index, item ->
SwipeToDismissListItem(
modifier = Modifier.animateItemPlacement(),
onEndToStart = {
val mutableList = myList.toMutableList()
mutableList.removeAt(index)
myList = mutableList.toList()
},
) {
ListItem(
headlineContent = {
Text(
text = item.description,
style = MaterialTheme.typography.titleMedium
)
},
leadingContent = {
Text(
text = item.emoji,
style = MaterialTheme.typography.displayMedium
)
}
)
}
if (myList.size != index) {
HorizontalDivider()
}
}
}
Bringing Familiarity to Cross-Platform Users
By introducing this swipe gesture, common in iOS, into Android apps, we’re not just adding a new feature; we’re building a bridge of familiarity for users traversing between these two platforms. This is the power of Material Design: creating consistent, intuitive experiences that users recognize and love, regardless of the platform they’re using.