You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中LazyColumn实现嵌套列表崩溃问题求助

问题描述

我正尝试在Jetpack Compose中构建一个包含嵌套列表的界面,效果如下:
界面预览

用于构建预览的代码:

@Preview
@Composable
fun ChannelBountiesCardPreview() {
    Column {
        Text(
            text = "ACS Microfinance - *614*435# - NG".uppercase(),
            modifier = Modifier
                .fillMaxWidth()
                .padding(dimensionResource(id = R.dimen.margin_13)),
            style = MaterialTheme.typography.body1,
            fontWeight = FontWeight.Bold,
            textAlign = TextAlign.End
        )

        repeat(3) {
            BountyCardPreview()
        }
    }
}

@Preview
@Composable
fun BountyCardPreview() {
    val margin13 = dimensionResource(id = R.dimen.margin_13)
    val margin8 = dimensionResource(id = R.dimen.margin_8)

    Column(modifier = Modifier.background(color = colorResource(id = R.color.colorSurface)).padding(vertical = margin8)) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = margin13),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text(
                text = "Check Balance",
                modifier = Modifier
                    .padding(top = margin8, bottom = margin8, end = margin13),
                style = MaterialTheme.typography.body1
            )

            Text(
                text = "USD $1",
                modifier = Modifier
                    .padding(top = margin8, bottom = margin8),
                style = MaterialTheme.typography.body1,
                fontWeight = FontWeight.Medium
            )
        }

        HorizontalImageTextView(
            drawable = R.drawable.ic_error,
            stringRes = R.string.bounty_transaction_failed,
            modifier = Modifier.padding(start = margin13, end = margin13, top = 5.dp, bottom = dimensionResource(id = R.dimen.margin_10)),
            MaterialTheme.typography.caption
        )
    }
}

@Preview
@Composable
fun BountiesPreview() {
    AppTheme {
        Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
            LazyColumn {
                items(5) {
                    ChannelBountiesCardPreview()
                }
            }
        }
    }
}

预览可正常渲染,但运行应用时崩溃,错误信息:

java.lang.IllegalStateException: Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()). If you want to add a header before the list of items please add a header as a separate item() before the main items() inside the LazyColumn scope. There are could be other reasons for this to happen: your ComposeView was added into a LinearLayout with some weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a custom layout. Please try to remove the source of infinite constraints in the hierarchy above the scrolling container.

实际运行的代码:

@Composable
fun BountyList(bountyViewModel: BountiesViewModel) {
    val bountiesState by bountyViewModel.bountiesState.collectAsState()

    AppTheme {
        Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
            LazyColumn {
                items(bountiesState.bounties) { channelBounty ->
                    ChannelBountyCard(channelBounty = channelBounty)
                }
            }
        }
    }
}

@Composable
fun ChannelBountyCard(channelBounty: ChannelBounties) {
    Column {
        Text(
            text = channelBounty.channel.ussdName.uppercase(),
            modifier = Modifier
                .fillMaxWidth()
                .padding(dimensionResource(id = R.dimen.margin_13)),
            style = MaterialTheme.typography.body1,
            fontWeight = FontWeight.Bold,
            textAlign = TextAlign.End
        )

        channelBounty.bounties.forEach {
            BountyCard(bounty = it)
        }
    }
}

@Composable
fun BountyCard(bounty: Bounty) {
    val context = LocalContext.current
    val margin8 = dimensionResource(id = R.dimen.margin_8)
    val margin13 = dimensionResource(id = R.dimen.margin_13)

    val bountyState = getBountyState(bounty)

    val strikeThrough = TextStyle(
        fontFamily = Brutalista,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        textDecoration = TextDecoration.LineThrough
    )

    Column(
        modifier = Modifier
            .background(color = colorResource(id = bountyState.color))
            .padding(vertical = margin8)
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = margin13),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text(
                text = bounty.generateDescription(context),
                modifier = Modifier
                    .padding(top = margin8, bottom = margin8, end = margin13),
                style = if (bountyState.isOpen) MaterialTheme.typography.body1 else strikeThrough
            )

            Text(
                text = stringResource(R.string.bounty_amount_with_currency, bounty.action.bounty_amount),
                modifier = Modifier
                    .padding(top = margin8, bottom = margin8),
                style = if (bountyState.isOpen) MaterialTheme.typography.body1 else strikeThrough,
                fontWeight = FontWeight.Medium
            )
        }

        if (bountyState.msg != 0)
            HorizontalImageTextView(
                drawable = bountyState.icon,
                stringRes = bountyState.msg,
                modifier = Modifier
                    .padding(start = margin13, end = margin13, top = 5.dp, bottom = dimensionResource(id = R.dimen.margin_10)),
                MaterialTheme.typography.caption
            )
    }
}

@Composable
internal fun HorizontalImageTextView(
    @DrawableRes drawable: Int,
    @StringRes stringRes: Int,
    modifier: Modifier = Modifier, textStyle: TextStyle
) {
    Row(horizontalArrangement = Arrangement.Start, modifier = modifier) {
        Image(
            painter = painterResource(id = drawable),
            contentDescription = null,
            modifier = Modifier.align(Alignment.CenterVertically),
        )
        Text(
            text = Html.fromHtml(stringResource(id = stringRes), HtmlCompat.FROM_HTML_MODE_LEGACY).toString(),
            style = textStyle,
            modifier = Modifier
                .padding(start = dimensionResource(id = R.dimen.margin_13))
                .align(Alignment.CenterVertically),
            color = colorResource(id = R.color.offWhite)
        )
    }
}

现有嵌套列表方案多针对可展开列表,我的需求是所有列表项始终可见,求解决办法。


解决方案

崩溃原因是LazyColumn内部的Column没有明确的高度约束,导致子项被赋予无限高度的测量条件,触发了Compose的滚动组件测量限制。

不需要嵌套滚动容器,直接把所有层级的内容平铺到外层的LazyColumn中即可,保持所有项始终可见:

方案1:直接整合内容到LazyColumn

修改BountyList的实现,将频道标题和对应的每个BountyCard都作为LazyColumn的独立项:

@Composable
fun BountyList(bountyViewModel: BountiesViewModel) {
    val bountiesState by bountyViewModel.bountiesState.collectAsState()

    AppTheme {
        Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
            LazyColumn {
                bountiesState.bounties.forEach { channelBounty ->
                    // 添加频道标题作为单独的item
                    item {
                        Text(
                            text = channelBounty.channel.ussdName.uppercase(),
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(dimensionResource(id = R.dimen.margin_13)),
                            style = MaterialTheme.typography.body1,
                            fontWeight = FontWeight.Bold,
                            textAlign = TextAlign.End
                        )
                    }
                    // 添加该频道下的所有BountyCard
                    items(channelBounty.bounties) { bounty ->
                        BountyCard(bounty = bounty)
                    }
                }
            }
        }
    }
}

// 删除原来的ChannelBountyCard组件,因为内容已经整合到LazyColumn中

方案2:保持封装性的扩展函数实现

如果需要保留代码模块化,可以将频道相关的列表项封装为LazyListScope的扩展函数:

fun LazyListScope.ChannelBountyItems(channelBounty: ChannelBounties) {
    item {
        Text(
            text = channelBounty.channel.ussdName.uppercase(),
            modifier = Modifier
                .fillMaxWidth()
                .padding(dimensionResource(id = R.dimen.margin_13)),
            style = MaterialTheme.typography.body1,
            fontWeight = FontWeight.Bold,
            textAlign = TextAlign.End
        )
    }
    items(channelBounty.bounties) { bounty ->
        BountyCard(bounty = bounty)
    }
}

// 在BountyList中调用扩展函数
@Composable
fun BountyList(bountyViewModel: BountiesViewModel) {
    val bountiesState by bountyViewModel.bountiesState.collectAsState()

    AppTheme {
        Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
            LazyColumn {
                bountiesState.bounties.forEach { channelBounty ->
                    ChannelBountyItems(channelBounty)
                }
            }
        }
    }
}

两种方案都能解决问题,同时满足所有列表项始终可见的需求,还保留了LazyColumn的懒加载性能优势。

内容的提问来源于stack exchange,提问作者Alex Kombo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 21:24:23