如何为API返回的JSON数据实现分组LazyColumn列表
嵌套LazyColumn滚动失效问题解决(List of Lists of Objects分组展示)
问题背景
需要将API返回的List of Lists of Objects格式数据在LazyColumn中分组展示,当前实现因嵌套LazyColumn导致列表无法正常滚动,相关代码及数据格式如下:
问题代码示例
@Composable fun CupStandingExpandableGroup( mainViewModel: MainViewModel = hiltViewModel(), standingsItemList: List<StandingsItemItem?>, standingsItemListOfList: List<List<StandingsItemItem?>?>?, headerItem: @Composable ()-> Unit, keys: @Composable ()-> Unit ) { var expandedState by remember { mutableStateOf(true) } val rotationState by animateFloatAsState( targetValue = if (expandedState) 180f else 0f, label = "" ) Column { standingsItemListOfList!!.forEach { mainViewModel.standingsListTournament.value = it!! val groupedStandingsList = mainViewModel.standingsListTournament.value.groupBy { it!!.group } LazyColumn { item { headerItem() } groupedStandingsList.forEach { (group, items) -> item { Card( modifier = Modifier .fillMaxWidth() .animateContentSize( animationSpec = tween( durationMillis = 3000, easing = LinearOutSlowInEasing ) ), shape = RoundedCornerShape(15.dp), onClick = { expandedState = !expandedState } ) { Column( modifier = Modifier .fillMaxWidth() .padding(4.dp) ) { Row( verticalAlignment = Alignment.CenterVertically ) { Text( modifier = Modifier .weight(6f), text = group!!, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis ) IconButton( modifier = Modifier .weight(1f) .alpha(ContentAlpha.medium) .rotate(rotationState), onClick = { expandedState = !expandedState }) { Icon( imageVector = Icons.Default.ArrowDropDown, contentDescription = "Drop-Down Arrow" ) } } } } } if (expandedState) { itemsIndexed( items, itemContent = { index, item -> StandingRow(standingsItem = item!!) } ) } } item { keys() } } } } }
List of Lists of Objects数据格式示例
[ [ { }, { } ], [ { }, { } ], [ { }, { } ] ]
单列表数据的正常分组实现示例(对比参考)
val list = viewModel.mainList.value.groupBy { it!!.league!!.id } if (myList.isNotEmpty()) { LazyColumn( modifier = Modifier .padding(3.dp) .fillMaxSize() ) { myList.forEach { (league, items) -> item { TodayMatchesHeader(leagueItem = items[0]!!, league = league!!) } itemsIndexed(items, itemContent = { index, item -> TodayMatchesRow( item, onClick = { // 点击事件逻辑 } ) } ) } } }
List of Objects数据格式示例(对比参考)
[ { }, { }, { } ]
问题分析
- 嵌套LazyColumn无高度限制:外层Column循环创建多个LazyColumn,每个LazyColumn默认会占据尽可能多的高度,导致滚动事件冲突,无法正常滚动。
- 全局展开状态错误:所有分组共用同一个
expandedState,点击任意分组的展开按钮会同时控制所有分组的展开/收起,不符合预期。
解决方案
核心修改点
- 移除嵌套的LazyColumn,改用单个根LazyColumn承载所有内容,避免滚动冲突。
- 为每个分组单独维护展开状态,而不是使用全局状态。
- 直接处理
standingsItemListOfList数据,无需借助ViewModel临时存储,简化逻辑。
修正后的代码
@Composable fun CupStandingExpandableGroup( standingsItemListOfList: List<List<StandingsItemItem?>?>?, headerItem: @Composable ()-> Unit, keys: @Composable ()-> Unit ) { // 过滤掉空列表和空元素,简化后续处理 val validData = standingsItemListOfList?.filterNotNull()?.flatten()?.filterNotNull() ?: emptyList() // 按group分组 val groupedStandingsList = validData.groupBy { it.group } LazyColumn( modifier = Modifier.fillMaxSize() ) { // 添加头部 item { headerItem() } // 遍历每个分组 groupedStandingsList.forEach { (group, items) -> // 为每个分组单独维护展开状态 var expandedState by remember { mutableStateOf(true) } val rotationState by animateFloatAsState( targetValue = if (expandedState) 180f else 0f, label = "" ) // 分组头部卡片 item { Card( modifier = Modifier .fillMaxWidth() .animateContentSize( animationSpec = tween( durationMillis = 300, // 缩短动画时长更自然 easing = LinearOutSlowInEasing ) ), shape = RoundedCornerShape(15.dp), onClick = { expandedState = !expandedState } ) { Column( modifier = Modifier .fillMaxWidth() .padding(4.dp) ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( modifier = Modifier.weight(6f), text = group ?: "未分组", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis ) IconButton( modifier = Modifier .weight(1f) .alpha(ContentAlpha.medium) .rotate(rotationState), onClick = { expandedState = !expandedState } ) { Icon( imageVector = Icons.Default.ArrowDropDown, contentDescription = "展开/收起" ) } } } } } // 展开时显示分组内的列表项 if (expandedState) { itemsIndexed(items) { _, item -> StandingRow(standingsItem = item) } } } // 添加底部的keys item { keys() } } }
额外优化说明
- 数据预处理:先过滤掉空列表和空元素,避免后续频繁处理
null值,提升代码健壮性。 - 独立展开状态:每个分组通过
remember单独维护expandedState,确保操作互不影响。 - 简化动画时长:将动画时长从3000ms缩短到300ms,交互更流畅自然。
- 移除不必要的ViewModel依赖:直接在Composable内处理数据,无需借助ViewModel临时存储,减少耦合。
内容的提问来源于stack exchange,提问作者Mahmoud Nabil
相关产品推荐
相关产品推荐

