ExpandableListView under 2 mins using jetpack compose
Recently while working on a requirement I stumbled upon a task that needed the ExpandableListView functionality from the old XML days. This is when I realized that there isn't really a composable in the Jetpack compose library that provides this solution. But looking at all the things that are available in the Compose library, I figured it shouldn't be hard to create our own custom composable that expands and hides its children.
In this article, we will touch upon the following points:
- Creating custom composable functions with child composables.
- Stateful composable and state handling inside them.
- Adding simple visibility animation to a composable in Jetpack Compose.
So let's get started with it.
Components with children
Well, this should be an easy task. If we look at all the other composables that the Jetpack Compose library has then we can clearly see a pattern that they use. Most of the composables like Column
, and Row
, have a content parameter as their last parameter which itself is a composable function.
They are basically called Slots
, where some other composable can be inserted inside a hosting composable. So let's create an ExpandableContainer composable.
@Composable
fun ExpandableContainer(
title: String,
content: @Composable ColumnScope.() -> Unit,
) {
Column(verticalArrangement = Arrangement.Center) {
Text(text = title, style = heading2b)
content()
}
}
As you can see we have added the content parameter to this Composable function which is also a Composable function. I have also added a title parameter which will act as the heading for this section. This implementation then allows us to pass a child content that adds a title text to it.
The Slots
are an important aspect of Jetpack Compose since they enforce the single responsibility principle. For instance, our ExpandableContainer should not care about what the content actually is. Its main job is to display a title and show or hide the content based on its state.
Note:
For the purpose of focussing more on understanding the functionality, we are omitting the UI beautification for the time being.
State Handling for Content Visibility
We wouldn't want the content to be visible at all times, we should only show the content when the user taps on it. We can also optionally take an initial state parameter to decide if the starting state is expanded or collapsed for the composable.
To implement this we need to introduce a state for this composable which should be an internal state and shouldn't be accessible/controlled from outside (If need be you could have it controlled, but I feel like its an overkill since this component should itself handle its content visibility state).
enum class ExpandableState { VISIBLE, HIDDEN }
@Composable
fun ExpandableContainer(
title: String,
defaultState: ExpandableState = ExpandableState.HIDDEN,
content: @Composable ColumnScope.() -> Unit,
) {
//State
var isContentVisible by rememberSavable { mutableStateOf(defaultState) }
Column(
modifier = Modifier
.fillMaxWidth()
.clickable {
isContentVisible =
if (isContentVisible == ExpandableState.VISIBLE) ExpandableState.HIDDEN else ExpandableState.VISIBLE
},
verticalArrangement = Arrangement.Center
) {
Text(text = title, style = heading2b)
AnimatedVisibility(visible = isContentVisible == ExpandableState.VISIBLE) {
Column {
content()
}
}
}
}
Let's break down some of the things that are happening in the code written above.
isContentVisible
is the state that controls whether the content is visible or not. When the user presses the column then we toggle the visibility state of the composable. Notice I am using rememberSavable here, you can use remember as well in this case but rememberSavable survives view recycling in a LazyColumn.AnimatedVisibility
is a composable that can toggle the visibility of its child component. It animates the appearance and disappearance of its content. If You look at the internal implementation ofAnimatedVisibility
, you will notice that the default animation for the content isfadeIn()/fadeOut() + expandVertically()/shrinkVertically()
. This is perfect for our use case since we are making an expanding container. Also, notice I created another Column inside theAnimatedVisibility
to wrap the content. This is done because by defaultAnimatedVisibility
doesn't have any Layout of its own. The content will be rendered on top of one another, just like a Box.Lastly we have an enum
ExpandableState
which is a simple representation of the visibility state of the container.
With this, we now have an Expanding/Collapsing container that can be used in LazyLists or simple Column composable. Here is an implementation for using the above composable.
@Composable
fun MainActivityView() {
LazyColumn(Modifier.fillMaxSize()) {
item {
ExpandableContainer(
title = "Network type",
) {
//Some view that should be shown when the item is clicked.
}
Divider()
}
}
}
Conclusions
Even though there might not be an exact mapping of the XML based components in Jetpack Compose, we can still very easily make use of the Jetpack Compose lib to create our own custom composables. The Slots
API demonstrated above is a very powerful way of reusing functionality and should be used when creating global composable functions.
Also the AnimatedVisibility
composable is very easy to use and comes with inbuilt animations.
If you think this article has helped you understand Jetpack Compose better, consider sharing this article with your friends.
Subscribe to my newsletter
Read articles from Bawender Yandra directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Bawender Yandra
Bawender Yandra
Experienced professional with over 8 years of development in the mobile and web domain. I like writing and exploring different techniques of modern application development.