1

I am trying to build a screen that contains nested lists as in the image below.

Preview

The code that builds the preview above is:

@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()
                }
            }
        }
    }
}

While the preview renders successfully, when the app is run, it crashes with the following error:

java.lang.IllegalStateException: Vertically scrollable component was measured with an infinity maximum height constraints, which i
                            s disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()). If you want to a
                            dd 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 t
                            he hierarchy above the scrolling container.

The code for the screen is:

@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)
        )
    }
}

None of the existing solutions for nested loops work. Most of them deal with expandable lists while my use case needs the list of items to be visible at all times.

Alex Kombo
  • 3,256
  • 8
  • 34
  • 67
  • Since your screen code snippet works without an error for me, I am assuming it might be related to `HorizontalImageTextView` since that is the only component I had to comment out because it is not provided in your example. Also, just to rule out version differences, on which Compose version are you getting this error? – Ma3x Jul 25 '22 at 07:33
  • @Ma3x I've edited the code sample with it. It's just a `textview` with an `image`. I'm on compose v1.1.1. My issue is that the code runs fine on the live editor but fails when running with actual data. – Alex Kombo Jul 25 '22 at 08:12

1 Answers1

1

I found out that since I was using XML and compose interoperably, LazyColumn cannot have a scrollable parent. In my case it was a NestedScrollView. Removing the parent and rewriting the whole UI in compose fixed the problem.

Alex Kombo
  • 3,256
  • 8
  • 34
  • 67