Commonly used useful features in ListView: Swipe Refresh, Shimmer Effect, Reveal Menu with Jetpack Compose

Emre Karataş
8 min readMay 9, 2024

--

When I first learned Compose, the first example I created was a ListView because I have this perception that the better a programming language allows me to create a ListView quickly and comfortably, the better it is :)

In this article, we will focus on 3 features that are frequently used in modern applications and that I think are very useful:

1- Setting and reloading the data (Swipe Refresh)
2- Animation during loading (Shimmer Effect)
3- Action for each item (Reveal Menu)

Let’s start with item number one.

1- Setting and reloading the data (Swipe Refresh)

LazyColumn(modifier = Modifier.fillMaxSize()){
items(100){
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
.fillMaxWidth()
.padding(20.dp)) {
Icon(imageVector = Icons.Default.Call, contentDescription = null, modifier = Modifier.size(40.dp))
Spacer(modifier = Modifier.width(15.dp))
Column {
Text("User ${it+1}")
Text("Phone Number")
}
}
}
}

LazyColumn Component

  • LazyColumn: This is a list view that acts as the equivalent of RecyclerView in Compose. It can efficiently display a large amount of data. “Laziness” means that only the items visible on the screen are rendered.
  • items: Creates 100 items for the list. Each item is organized within a Row.

Now, let’s add the swipe refresh feature to this code with a small touch.

ComposeListExampleTheme {
val viewModel = viewModel<MainViewModel>()
val isLoading by viewModel.isLoading.collectAsState()
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isLoading)

SwipeRefresh(state = swipeRefreshState, onRefresh = viewModel::loadStuff, indicator = {
state, refresh ->
SwipeRefreshIndicator(state = state, refreshTriggerDistance = refresh, backgroundColor = Color(0xFF63358B),
contentColor = Color.White)
})
{

LazyColumn(modifier = Modifier.fillMaxSize()){
items(100){
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
.fillMaxWidth()
.padding(20.dp)) {
Icon(imageVector = Icons.Default.Call, contentDescription = null, modifier = Modifier.size(40.dp))
Spacer(modifier = Modifier.width(15.dp))
Column {
Text("User ${it+1}")
Text("Phone Number")
}
}
}
}

}
}

Let’s take a closer look at the code. I added a ViewModel and delegated the responsibility of the isLoading parameter there.

The rememberSwipeRefreshState function creates a state object in the Jetpack Compose UI library to be used with the SwipeRefresh component. This object stores and manages the current state of the swipe-to-refresh component. Specifically, it is used to manage the state when the user initiates a data refresh by performing a swipe gesture.

val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isLoading)

This code snippet creates an object that holds the current refresh state of the SwipeRefresh component. The isRefreshing parameter takes a Boolean value and represents the refresh state of the SwipeRefresh component. If this value is true, a refresh process is displayed in the user interface (usually as a spinning loading icon). If it is false, it indicates that there is no refresh activity.

SwipeRefresh Component

  • SwipeRefresh: This component allows users to perform a pull gesture to easily refresh the content.
  • state: Controls the state of SwipeRefresh, such as tracking when the user begins and completes the pull action.
  • onRefresh: This function is triggered when the user pulls down to refresh the list. In this example, the viewModel::loadStuff function serves this purpose and changes the value of the isLoading parameter after a 2-second delay in the ViewModel.
fun loadStuff(){
viewModelScope.launch {
_isLoading.value = true
delay(2000L)
_isLoading.value = false
}
}
  • indicator: This is the visual indicator shown during the SwipeRefresh gesture. The indicator becomes visible when pulled to a certain extent, providing visual feedback to the user. The backgroundColor and contentColor are set to adjust the background and content color.

2- Animation during loading (Shimmer Effect)

We’ve seen how the list is created and how the swipe refresh feature is added up to this point. Now, let’s move on to item number two: the Shimmer Effect.

The “Shimmer” effect provides a visual loading animation, particularly for content that is being loaded. This effect is commonly used to visually enhance the loading state until the data is fully loaded, making it more appealing. The code alternates between showing a placeholder view with a “shimmer” effect or the actual content, depending on conditions. Let’s now examine the code piece by piece:

@Composable
fun ShimmerListItem(
isLoading: Boolean, contentAfterLoading: @Composable () -> Unit,
modifier: Modifier = Modifier
) {

if(isLoading){
Row ( modifier = modifier){

Box(modifier = Modifier
.size(100.dp)
.shimmerEffect())
Spacer(modifier = Modifier.width(16.dp))
Column ( modifier = Modifier.weight(1f)){

Box(modifier = Modifier.fillMaxWidth(1f).height(40.dp).shimmerEffect())
}
}
}
else{
contentAfterLoading()
}
}

fun Modifier.shimmerEffect(): Modifier = composed {
var size by remember {
mutableStateOf(IntSize.Zero)
}

val transition = rememberInfiniteTransition()
val startOffsetX by transition.animateFloat(initialValue = -2 * size.width.toFloat(), targetValue = 2 * size.width.toFloat(), animationSpec = infiniteRepeatable(
tween(durationMillis = 1000)
))

background(
brush = Brush.linearGradient(colors = listOf(Color(0xFF77717A), Color(0xFF5C4F66), Color(0xFF766D7E)), start = Offset(startOffsetX, 0f), end = Offset(startOffsetX + size.width.toFloat()
, size.height.toFloat()))
).onGloballyPositioned {
size = it.size
}
}

ShimmerListItem Function Parameters:

  • isLoading: Takes a Boolean value. If true, a loading animation is displayed; if false, the actual content is shown.
  • contentAfterLoading: A lambda function that defines the content to be displayed once the loading is complete. This can include a composable function.
  • modifier: A Modifier that defines UI modifications to be applied on the composable. The default value is Modifier.

Code Flow:

  • If isLoading is true, a Row is displayed containing components with the "shimmer" effect applied:
  • Box: Creates a square area and applies the shimmerEffect() modifier.
  • Spacer: Leaves space between the Box and the Column.
  • Column: Contains a rectangle area of height 40.dp, again with the shimmerEffect() applied.

shimmerEffect Function

  • This function returns a Modifier and adds a “shimmer” effect to the composable components it is applied on.
  • rememberInfiniteTransition(): Creates a transition object for the animation, which provides a continuously repeating animation.
  • animateFloat: Applies an animation between start and end values along the X-axis. The shimmer effect visually shifts left and right with this animation.
  • background: Applies a color gradient effect using a LinearGradient brush. The start and end points dynamically change depending on the current value of the startOffsetX animation.

We can apply our effect to the list items by wrapping them with ShimmerListItem.

LazyColumn(modifier = Modifier.fillMaxSize()){
items(100){
ShimmerListItem (
isLoading = isLoading,
contentAfterLoading
= {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
.fillMaxWidth()
.padding(20.dp)) {
Icon(imageVector = Icons.Default.Call, contentDescription = null, modifier = Modifier.size(40.dp))
Spacer(modifier = Modifier.width(15.dp))
Column {
Text("User ${it+1}")
Text("Phone Number")
}
}
}, modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(20.dp)
)
}
}

3- Action for each item (Reveal Menu)

Now, let’s move on to the final step: creating a reveal menu, which allows for actions on the items when swiped left or right in the list. This functionality enhances user interaction by providing additional options such as edit, delete, or more through a swipeable interface on each list item.

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeToRevealItem(content: @Composable () -> Unit) {
val swipeableState = rememberSwipeableState(initialValue = 0)
val size = with(LocalDensity.current) { 100.dp.toPx() }
val anchors = mapOf(0f to 0, -size to 1) // 0 is the original position, -size is the revealed position

val context = LocalContext.current

Box(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(0.9f) },
orientation = Orientation.Horizontal
)
) {
// Reveal area behind the item
Box(
modifier = Modifier
.fillMaxHeight()
.width(200.dp)
.align(Alignment.CenterEnd)
.background(Color.White)
) {
Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
Icon(imageVector = Icons.Default.Favorite, contentDescription = null, modifier = Modifier
.size(36.dp)
.clickable(onClick = {
Toast
.makeText(context, "User added to favorites", Toast.LENGTH_SHORT)
.show()
}),tint = Color.Green)
Spacer(modifier = Modifier.width(24.dp))
Icon(imageVector = Icons.Default.Email, contentDescription = null, modifier = Modifier
.size(36.dp)
.clickable(onClick = {
Toast
.makeText(context, "Message sent to user", Toast.LENGTH_SHORT)
.show()
}),tint = Color.DarkGray)
Spacer(modifier = Modifier.width(24.dp))
Icon(imageVector = Icons.Outlined.Delete, contentDescription = null, modifier = Modifier
.size(36.dp)
.clickable(onClick = {
Toast
.makeText(context, "User deleted", Toast.LENGTH_SHORT)
.show()
}),tint = Color.Red)
}
}

// Main content which moves with swipe
Box(
modifier = Modifier
.fillMaxSize()
.offset(x = swipeableState.offset.value.dp)
.background(Color.LightGray)
) {
content()
}
}
}

The SwipeToRevealItem composable function in the provided Kotlin code uses the Jetpack Compose library to implement a UI pattern where additional actions are revealed when a user swipes on an item. This can be particularly useful in mobile applications for tasks like deleting, favoriting, or sending messages directly from a list. Let’s examine the code to understand how it works.

Decorators and Setup

  • @OptIn(ExperimentalMaterialApi::class): This annotation is used to allow usage of APIs that are marked as experimental within the Material components. These APIs are not guaranteed to be stable and may change in future versions.
  • @Composable: Marks the function as a Composable, which means this function is meant to define UI in Jetpack Compose.

Function Signature

  • content: A composable lambda that represents the primary content of the list item.

Local Variables

  • swipeableState: It maintains the state of the swipe and holds information whether the swipeable panel is open or closed. Initially set to 0 (closed).
  • size: Represents the size of the swipeable area calculated in pixels.
  • anchors: Defines the positions (in pixels) where the swipeable can settle. Here, 0f means fully closed, and -size means fully open.

Components

1. Outer Box

  • This Box acts as the container for the swipeable item.
  • It is swipeable and fills the width of its parent and has a height of 80.dp.
  • The swipeable modifier takes swipeableState, anchors, a thresholds function to determine how far should the swipe be to consider it a swipe (90% here), and sets the orientation to horizontal.

2. Inner Box for Actions

  • Positioned on the right and appears when the item is swiped to the left.
  • It has a fixed width of 200.dp and fills the height of its container.
  • Inside, it contains a Row of Icons (Favorite, Email, Delete) which are clickable and show Toast messages when clicked. Each icon is styled and colored appropriately.

3. Content Box

  • This Box contains the content() composable that is passed to SwipeToRevealItem. It occupies the full size of its container.
  • The horizontal offset of this box is determined by swipeableState.offset.value.dp, making it move along with the swipe.

Swipe Behavior and Actions

  • The item can be swiped left to reveal actions. The amount of swipe required to trigger the action state fully is determined by the thresholds setting.
  • Actions to be performed are determined based on the actions added here. In my example, I only took action with a Toast message.

The provided code uses Jetpack Compose to implement a UI pattern in a mobile app interface, where users can swipe right on a list item to reveal additional actions. Additionally, it includes a “shimmer” effect that visually enriches the loading process and a swipe-to-refresh functionality that provides feedback during data loading. These features are particularly useful for tasks that can be performed directly from lists, such as adding to favorites, sending messages, or deleting items. The code outlines how the user interface is constructed and details how the swipe gesture, shimmer effect, and refresh function are configured. The SwipeToRevealItem function defines a space for actions to be displayed following a swipe, along with their user interactions. Users can quickly and effectively perform various actions by swiping these items. These structures create an effective user experience for modern mobile applications, providing quick access to frequently used actions.

Thank you for reading my article. I hope it has been a useful article 👋

--

--