A Jetpack Compose library that provides an expandable list view with headers, child items, selection support, and customizable styling.
- Expandable/collapsible headers with animated content reveal
- Item selection with visual feedback
- Header category icons support
- Annotated text support for list items (e.g., clickable links, styled spans)
- Long-click support on list items
- Fully customizable styling (colors, typography, padding, animations)
- Two API variants: standalone
@ComposableandLazyListScopeextension
Step 1: Add JitPack repository to your root settings.gradle.kts or build.gradle.kts:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}Step 2: Add the dependency to your module's build.gradle.kts:
dependencies {
implementation("com.github.Coderkube-App:ComposeExpandableListView:1.0.1")
}import com.coderkube.expandablelistview.ExpandableListData
import com.coderkube.expandablelistview.ListItemData
val expandableListData = listOf(
ExpandableListData(
headerText = "Vegetables",
listItems = listOf(
ListItemData(name = "Carrot", isSelected = false),
ListItemData(name = "Broccoli", isSelected = false),
ListItemData(name = "Spinach", isSelected = false)
),
isExpanded = false,
headerCategoryIcon = R.drawable.ic_vegetable // optional
),
ExpandableListData(
headerText = "Fruits",
listItems = listOf(
ListItemData(name = "Apple", isSelected = false),
ListItemData(name = "Banana", isSelected = false),
ListItemData(name = "Orange", isSelected = false)
),
isExpanded = false,
headerCategoryIcon = R.drawable.ic_fruits // optional
)
)class MyViewModel : ViewModel() {
private val _expandableListData = MutableStateFlow(initialData)
val expandableListData: StateFlow<List<ExpandableListData>> = _expandableListData.asStateFlow()
fun updateExpandStatus(index: Int, isExpanded: Boolean) {
_expandableListData.update { data ->
data.mapIndexed { i, item ->
if (i == index) item.copy(isExpanded = isExpanded) else item
}
}
}
fun listItemSelected(headerIndex: Int, itemIndex: Int, isSelected: Boolean) {
_expandableListData.update { data ->
data.mapIndexed { hIdx, header ->
if (hIdx == headerIndex) {
header.copy(listItems = header.listItems.mapIndexed { iIdx, item ->
if (iIdx == itemIndex) item.copy(isSelected = isSelected) else item
})
} else header
}
}
}
}ComposeExpandableListView(
modifier = Modifier.fillMaxWidth(),
expandableListData = uiState.simpleExpandableListData,
onStateChanged = mainViewModel::updateExpandStatus,
onListItemClicked = mainViewModel::listItemSelected,
onListItemLongClicked = this@MainActivity::listItemLongClicked
)If you want to use this component inside a LazyColumn, use the LazyListScope extension function:
LazyColumn {
composeExpandableListView(
expandableListData = uiState.simpleExpandableListData,
onStateChanged = mainViewModel::updateExpandStatus,
onListItemClicked = mainViewModel::listItemSelected,
onListItemLongClicked = this@MainActivity::listItemLongClicked
)
}Note: If you have a scrollable
Columnas a parent, convert it toLazyColumnand use theLazyListScope.composeExpandableListView()variant. This ensures proper lazy layout behavior.
ExpandableListData(
headerText = "Support",
listItems = listOf(
ListItemData(
annotatedText = buildAnnotatedString {
append("Visit our ")
withLink(
LinkAnnotation.Clickable(
tag = "help",
styles = TextLinkStyles(SpanStyle(color = Color.Blue)),
linkInteractionListener = { /* handle click */ }
)
) { append("help center") }
}
),
ListItemData(
annotatedText = AnnotatedString.fromHtml("Read our <b>FAQ</b> here")
)
)
)| Parameter Name | Parameter Type | Description | Default Value |
|---|---|---|---|
| expandableListData | List<ExpandableListData> |
List of ExpandableListData representing the expandable list's headers and child items. |
N/A |
| headerStylingAttributes | HeaderStylingAttributes |
Defines styling for headers, including appearance and layout. | defaultHeaderStylingAttributes |
| listItemStylingAttributes | ListItemStylingAttributes |
Defines styling for list items, including appearance and layout. | defaultListItemStylingAttributes |
| contentAnimation | ContentAnimation |
Defines expand/collapse animation for expandable list content. | defaultContentAnimation |
| expandedIcon | Int (drawable res ID) |
Icon when a header is expanded. | ic_arrow_right |
| collapseIcon | Int (drawable res ID) |
Icon when a header is collapsed. | ic_arrow_drop_down |
| itemSelectedIcon | Int (drawable res ID) |
Icon when an item is selected. | ic_check |
| onStateChanged | (headerIndex: Int, isExpanded: Boolean) -> Unit |
Callback for when a header's expand/collapse state changes. | empty lambda |
| onListItemClicked | (headerIndex: Int, itemIndex: Int, isSelected: Boolean) -> Unit |
Callback for when a list item is clicked. | empty lambda |
| onListItemLongClicked | (headerIndex: Int, itemIndex: Int, isSelected: Boolean) -> Unit |
Callback for when a list item is long pressed. | empty lambda |
data class HeaderStylingAttributes(
val backgroundColor: Color,
val cornerRadius: Dp,
val textStyle: TextStyle,
val headerPaddings: HeaderPaddings
)
data class ListItemStylingAttributes(
val backgroundColor: Color,
val selectedBackgroundColor: Color,
val textStyle: TextStyle,
val selectedTextStyle: TextStyle,
val contentPadding: PaddingValues
)
data class HeaderPaddings(
val categoryIconPadding: PaddingValues,
val textPadding: PaddingValues,
val actionIconPadding: PaddingValues
)
data class ContentAnimation(
val expandAnimation: EnterTransition,
val collapseAnimation: ExitTransition
)val customHeaderStyling = ExpandableListViewDefaults.defaultHeaderStylingAttributes.copy(
backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
cornerRadius = 12.dp
)
val customListItemStyling = ExpandableListViewDefaults.defaultListItemStylingAttributes.copy(
selectedBackgroundColor = MaterialTheme.colorScheme.tertiaryContainer,
selectedTextStyle = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)
)
ComposeExpandableListView(
expandableListData = data,
headerStylingAttributes = customHeaderStyling,
listItemStylingAttributes = customListItemStyling,
expandedIcon = R.drawable.ic_expand_less,
collapseIcon = R.drawable.ic_expand_more,
itemSelectedIcon = R.drawable.ic_check_circle,
onStateChanged = { index, expanded -> /* handle */ },
onListItemClicked = { hIdx, iIdx, selected -> /* handle */ },
onListItemLongClicked = { hIdx, iIdx, selected -> /* handle */ }
)@Stable
data class ExpandableListData(
val headerText: String,
val listItems: List<ListItemData>,
val isExpanded: Boolean = false,
@DrawableRes val headerCategoryIcon: Int? = null
)
@Stable
data class ListItemData(
val name: String? = null,
val annotatedText: AnnotatedString? = null,
val isSelected: Boolean = false
)Contributions are welcome! Feel free to submit issues or pull requests on the GitHub repository.
Copyright 2026 Coderkube
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.