Blog Infos
Author
Published
Topics
Published
Creating an expandable view with state using “remember”

In this part of our series on introducing Jetpack Compose into an existing project, we will build an expandable list item to use in our list of claims, a feature we are building using Jetpack Compose UI in our app at

.
First, let’s have a look again at the screen we are building.

 

 

We have already built the bottom buttons: “None of the above” and the “NEXT” Lemonade pink button.

Today we will learn how to build the expandable list item representing a group of claims for a pet insurance policy.

Let’s get started

Let’s start by looking at an isolated case of the item we are building.

Looking at our item at its collapsed state we can identify three components:

  • Text containing the title of the item.
  • Text containing the subtitle of the item.
  • An arrow icon on the right (Which will animate to rotate vertically when we expand the item).

The first thing we do, as always, is starting a composable:

@Composable
ExpandableListItem() {
   Card(...) {   
      Row(...) {
         TitleAndSubtitle(...)
         Icon(...) 
      }
   }
}

As we have seen many times before, when writing declarative UI’s, we first write as little as possible just to describe the result, or, what we want to see on the screen.

So to describe our expandable item we first write a Card that will contain aRow and inside we placeTitleAndSubtitle and the Icon.

The Card

Card is a composable supplied by the system, it can takeelevation and shape as parameters, and we can supply it with amodifier for anything else we need.
The Card is the container for our expandable list item so we will supply it with elevation , and somemodifiers :

  • fillMaxWidth() — will stretch our card the full width of the screen just like android:layout_width=”match_parent” would.
  • padding() — will give us space from the edges.
  • clip(RoundedCornerShape(*.dp)) — will clip the card to the supplied RoundedCornerShape .
  • clickable() — will supply a lambda we can later use to change the State of the composable
Card(
    elevation = 4.dp,
    modifier = Modifier
        .fillMaxWidth()
        .padding(start = 15.dp, top = 15.dp, end = 15.dp)
        .clip(RoundedCornerShape(8.dp))
        .clickable { // We will deal with the click event later }
)
The Row

In our Card we have a Row (Equivalent of a horizontal LineraLayout) that contains the titles and the icon.

The Row will take a horizontalArrangment parameter to space out the icon and the titles.

Row(
    horizontalArrangement = Arrangement.SpaceBetween,
    modifier = Modifier.fillMaxWidth()
) {...}
The title and subtitle

In the Row we place the TitleAndSubtitle and the Icon which will be aligned horizontally.

Row(...) {
    TitleAndSubtitle(
        title = "Expandable item title",
        subtitle = "There are more items below! 👇"
    )
    Icon(...)
}

The TitleAndSubtitle is a composable we wrote that has a Column(Equivalent of a vertical LineraLayout) with two Text composables

@Composable
fun TitleAndSubtitle(
    title: String,
    subtitle: String
) {
    Column(verticalArrangement = Arrangement.Center) {
        Text(text = title)
        Text(text = subtitle)
    }
}

Job Offers

Job Offers


    Android Build Engineer

    Pinterest
    San Francisco, CA | Seattle, WA
    • Full Time
    apply now

    Android App Developer

    sipgate GmbH
    Düsseldorf, Remote
    • Full Time
    apply now

    Senior Mobile Systems SDK Engineer

    Sauce Labs
    Remote
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

Jobs

The Icon

The Icon is a system composable, we will give it an imageVector which we get as part of the compose dependency, an align modifier and something new we haven’t seen before.. a graphicsLayer.

Icon(
    imageVector = Icons.Default.ArrowDropDown,
    modifier = Modifier
        .align(CenterVertically)
        .graphicsLayer(...)
)
The graphicsLayer modifier

GraphicsLayer is a modifier that is used to apply effects to content, such as scaling, rotation, opacity, shadow, and clipping.
In our case, we want to rotate the arrow icon so that when the item is expanding, the arrow will rotate to face up.

Inside graphicsLayer we can use animateFloatAsState which we have covered in the last article regarding the Lemonade pink button.
We will assign the rotationZ property here to achieve the rotation from 0to 180 degrees.

Icon(
   modifier = Modifier
      .graphicsLayer(
          rotationZ = animateFloatAsState(
                         if (expanded) 180f else 0f).value,
      )
)

The rotation is decided based on a boolean named expanded, this will be our components state.

Let’s talk about State

In Jetpack Compose, the declarative UI framework will render again only elements that had their state changed, when we talk about state in Compose UI context we usually refer to some function variables that hold or represents, that specific state.

This could be a primitive or an object.
and for our composable function to hold state, it needs to remember it across recomposition (re-rendering), otherwise, the state will be reset on each composition.

Our item is expandable, therefore it needs to hold a state that declares whether or not it is expanded.

To do that we will go back to our initial composable definition and add a state variable

@Composable
ExpandableListItem() {
var expanded by remember { mutableStateOf(false) }  
   Card(modifier = Modifier.clickable { expanded = !expanded}) {   
      Row(...) {
         TitleAndSubtitle(...)
         Icon(...) 
      }
   }
}
mutableStateOf

We will take the initial state of the item which is false (expanded = false) and pass it as a default value to mutableStateOf:

var expanded by remember { mutableStateOf(false) }

mutableStateOf is a reactive stream, similar to LiveDate or StateFlow and will emit boolean values as we change the expanded variable.

remember

We wrap the mutableStateOf stream in a remember block so that it will be remembered across recompositions, otherwise, the state will default to falseevery time ExpandableListItem is called.

We change the state when clicking the Card using the clickable modifier:
Card(modifier = Modifier.clickable { expanded = !expanded})

Now let’s implement the expandable part of the component.

The expanded state

Now we need to add another section on the bottom of our component,

Let’s wrap the Row element in a Column so we can place the extra items element below it.

@Composable
ExpandableListItem() {
var expanded
   Card(...) { 
        /// Wrap the row in a Column
        Column { 
            Row(...) {
               TitleAndSubtitle(...)
               Icon(...) 
            }
            
        // And add the extra items sections      
            Column {
              ExtraItem(item = Item())
              ExtraItem(item = Item())
            }
        }
    }
}
data class Item (
   val title: String,
   val date: String
)

Where ExtraItemis just a Row with two Text composables.

@Composable
fun ExtraItem(item: Item) {
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Text(text = item.title)
        Text(text = item.date)
    }
}

Now let’s add some styling to our expandable content with some Dividers and Spacers:

//// Extra items sections
    
    Divider(modifier = Modifier.height(1.dp))
    Column(modifier = Modifier.padding(...)) {

        Spacer(modifier = Modifier.height(10.dp))
        ExtraItem(item = Item())

        Spacer(modifier = Modifier.height(10.dp))
        Divider(modifier = Modifier.height(1.dp))
        Spacer(modifier = Modifier.height(10.dp))

        ExtraItem(item = Item())

        Spacer(modifier = Modifier.height(10.dp))
        Divider(modifier = Modifier.height(1.dp))
        Spacer(modifier = Modifier.height(10.dp))

        ExtraItem(item = Item())

    }

And now we get this beautiful component!

 

But clicking on this does nothing 😩, we are still missing that key ingredient in our component’s magic!.

Animate visibility with ease

One of the most delightful components that the compose UI dependency includes is the AnimatedVisibility composable.

@Composable
fun ExpandableListItem() {
    var expanded by remember { mutableStateOf(false) }
    Card() {
        Row() {
            TitleAndSubtitle()
            Icon()
        }
        AnimatedVisibility(visible = expanded) {
            Column {
                ExtraItem(item = Item())
                ExtraItem(item = Item())
            }
        }        
    }
}

The AnimatedVisibility allows us to have visibility animation changes to layouts like Column or LazyColumn, and according to the declarative UI paradigm, we will not have to define any custom animations to get this behavior.
However, we can easily add customizations to it if we would like by using AnimationSpec.

AnimationSpec

The AnimationSpec stores the specification of animations, we can use it to define easing, duration, delays, and other specs regarding animation customizations.
If we want animations such as expand and shrink animations for our UI we can use it like.

AnimatedVisibility(
    visible = expanded,
    enter = expandVertically(
        animationSpec = tween(
              durationMillis = 300, easing = FastOutLinearInEasing)
    ),
    exit = shrinkVertically(
        animationSpec = tween(
             durationMillis = 300, easing = FastOutLinearInEasing)
    )
)

And by adding these specifications to our AnimatedVisibility we can get the animation we desire for our expandable item!

Here is the complete code for our expandable item!

@Composable
fun ExpandableListItem() {
var expanded by remember { mutableStateOf(true) }
Card(
elevation = 4.dp,
modifier = Modifier
.fillMaxWidth()
.padding(start = 15.dp, top = 15.dp, end = 15.dp)
.clip(RoundedCornerShape(8.dp))
.clickable(onClick = { expanded = !expanded })
) {
Column(modifier = Modifier.fillMaxWidth().background(colorResource(id = R.color.lemonade_glass))) {
Row(horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(end = 32.dp)
.fillMaxWidth()
) {
TitleAndSubtitle(
title = "Expandable item title",
subtitle = "There are more items below! 👇"
)
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = "ArrowIcon",
modifier = Modifier
.align(CenterVertically)
.graphicsLayer(
rotationZ = animateFloatAsState(
if (expanded) 180f else 0f
).value,
)
)
}
AnimatedVisibility(
visible = expanded,
enter = expandVertically(
animationSpec = tween(durationMillis = 300, easing = FastOutLinearInEasing)
),
exit = shrinkVertically(
animationSpec = tween(durationMillis = 300, easing = FastOutLinearInEasing)
)
) {
Divider(modifier = Modifier.height(1.dp))
Column(modifier = Modifier.padding(start = 15.dp, end = 15.dp, bottom = 15.dp)) {
Spacer(modifier = Modifier.height(10.dp))
ExtraItem(item = Item(
"Look at the date here",
"March 2, 1998")
)
Spacer(modifier = Modifier.height(10.dp))
Divider(modifier = Modifier.height(1.dp))
Spacer(modifier = Modifier.height(10.dp))
ExtraItem(item = Item(
"Message about stuff",
"June 21, 1982")
)
Spacer(modifier = Modifier.height(10.dp))
Divider(modifier = Modifier.height(1.dp))
Spacer(modifier = Modifier.height(10.dp))
ExtraItem(item = Item(
"Look at the date here",
"March 2, 1998")
)
}
}
}
}
}
And that’s it!

We learned a lot today! we animated the arrow using graphicsLayermodifier, we animated visibility with AnimatedVisibility, and we learned how to use state in Jetpack Compose with remember and mutableStateOf.

In the next part, we will add a custom radio button to the expandable item!

Don’t forget to clap and subscribe, but only if you think I deserve it!.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
blog
Yes! You heard it right. We’ll try to understand the complete OTP (one time…
READ MORE
blog
The dictionary defines the term side-effect as an undesirable effect. While this applies to…
READ MORE

Leave a Reply

Your email address will not be published.

Fill out this field
Fill out this field
Please enter a valid email address.

Menu