14

Currently in Jetpack Compose, this code throws an IllegalStateException because you cannot nest two vertically scrolling Composables:

@ExperimentalFoundationApi
@Composable
fun MyLazyColumn() {
    LazyColumn {
        item {
            Text(text = "My LazyColumn Title")
        }
        item {
            LazyVerticalGrid(cells = GridCells.Fixed(4)) {
                items(10) {
                    Box(
                        modifier = Modifier
                            .size(50.dp)
                            .padding(5.dp)
                            .background(Color.Gray)
                    )
                }
            }
        }
    }
}

I do not want the grid itself to scroll, but simply display a fixed grid of the Composable that I pass into it. Is there any workaround to displaying a non-scrolling grid inside a LazyColumn?

Abhimanyu
  • 11,351
  • 7
  • 51
  • 121
Majesty
  • 165
  • 1
  • 6

5 Answers5

10

If you don't mind using an unstable API, you can use LazyVerticalGrid and make item take the full width with the span parameter, as @Mustafa pointed out:

LazyVerticalGrid(
    cells = GridCells.Fixed(spanCount),
) {
    item(
        span = { GridItemSpan(spanCount) }
    ) {
        Text("Title")
    }
    items(10) {
        Text(it.toString())
    }
}

Until it's stable, it's recommended

using stable components like LazyColumn and Row to achieve the same result.

It can be done by implementing gridItems to be used with LazyColumn.

fun LazyListScope.gridItems(
    count: Int,
    nColumns: Int,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    itemContent: @Composable BoxScope.(Int) -> Unit,
) {
    gridItems(
        data = List(count) { it },
        nColumns = nColumns,
        horizontalArrangement = horizontalArrangement,
        itemContent = itemContent,
    )
}

fun <T> LazyListScope.gridItems(
    data: List<T>,
    nColumns: Int,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    key: ((item: T) -> Any)? = null,
    itemContent: @Composable BoxScope.(T) -> Unit,
) {
    val rows = if (data.isEmpty()) 0 else 1 + (data.count() - 1) / nColumns
    items(rows) { rowIndex ->
        Row(horizontalArrangement = horizontalArrangement) {
            for (columnIndex in 0 until nColumns) {
                val itemIndex = rowIndex * nColumns + columnIndex
                if (itemIndex < data.count()) {
                    val item = data[itemIndex]
                    androidx.compose.runtime.key(key?.invoke(item)) {
                        Box(
                            modifier = Modifier.weight(1f, fill = true),
                            propagateMinConstraints = true
                        ) {
                            itemContent.invoke(this, item)
                        }
                    }
                } else {
                    Spacer(Modifier.weight(1f, fill = true))
                }
            }
        }
    }
}

Usage:

LazyColumn {
    item {
        Text(text = "My LazyColumn Title")
    }
    // with count
    gridItems(10, nColumns = 4) { index -> 
        Box(
            modifier = Modifier
                .size(50.dp)
                .padding(5.dp)
                .background(Color.Gray)
        )
    }
    // or with list of items
    gridItems(listOf(1,2,3), nColumns = 4) { item ->
        Box(
            modifier = Modifier
                .size(50.dp)
                .padding(5.dp)
                .background(Color.Gray)
        )
    }
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • 1
    This solution works fine in case you have a small list of data and simple UI for grid items, but when your data is large and grid's items have a complex UI and state, it will become a laggy and heavy. – Mustafa Ibrahim Feb 09 '22 at 09:39
  • @MustafaIbrahim Have you tried running a Release version of the app? `LazyColumn` is much less performant in Debug. Also have you tried plain single cell `LazyColumn` with the same layout? I don't think my approach adds much overhead to plain `LazyColumn` – Phil Dukhov Feb 10 '22 at 13:18
  • @MustafaIbrahim `LazyVerticalGrid` should perform better than my solution since **1.2.*** because it was rewritten from using `LazyColumn`, but in **1.1.*** I expect performance to be about the same. – Phil Dukhov Feb 10 '22 at 14:31
6

This accepted answer is great and works fine in case you have a small list of data and simple UI for grid items, but when your data is large and grid's items have a complex UI and state, it will become a laggy and heavy.

In my case , I manage to solve this issue by making the LazyVerticalGrid as the container for grid's items and also the other content , now it looks like this :

val spanCount = 2
LazyVerticalGrid(
    modifier = modifier
        .fillMaxWidth(),
    cells = GridCells.Fixed(spanCount),
    state = rememberLazyGridState(),
) {
    item(
        span = {
            /** Take a full row */
            GridItemSpan(currentLineSpan = spanCount)
        }
    ) {
        Column(
            modifier = Modifier.fillMaxWidth()
        ) {
            items.forEach {
                /** Iterate over your items here */
            }
        }
    }
    items(
        items = gridItems
    ){gridItem->
        /** Grid items go here */
    }
}
Mustafa Ibrahim
  • 557
  • 3
  • 8
2

For me I have added height explicity and it worked fine with me for LazyverticalGrid inside lazy colum

 LazyVerticalGrid(
        cells = GridCells.Fixed(3),
        contentPadding = PaddingValues(5.dp),
        modifier = Modifier
            .layoutId("lazy_list")
            .height(200.dp)
Ejaz Ahmad
  • 614
  • 5
  • 12
0

If your grid is small and not too complex (e.g. requires lazy loading) you can use a jetpack compose ConstraintLayout. I ran into this when having a scrollable parent which I could not modify.

It works pretty much the same as the view based ConstraintLayout though a little more limited.

E.g this would be a bullet point grid which keeps the text rows aligned to the longest bullet width:

val spaceAfterBullet = 8.dp
val spaceBetweenRows = 4.dp

ConstraintLayout(modifier = Modifier.fillMaxWidth()) {

    val (bullet1, bullet2, bullet3, text1, text2, text3) = createRefs()
    val bulletBarrier = createEndBarrier(bullet1, bullet2, bullet3)
    val row1Barrier = createBottomBarrier(bullet1, text1)
    val row2Barrier = createBottomBarrier(bullet2, text2)

    Text(text = "1.", textAlign = TextAlign.End, modifier = Modifier.constrainAs(bullet1) {
    })
    Text(text = "First line of text", modifier = Modifier
        .padding(start = spaceAfterBullet)
        .constrainAs(text1) {
            start.linkTo(bulletBarrier)
        })

    Text(text = "2.", textAlign = TextAlign.End, modifier = Modifier.constrainAs(bullet2) {
        top.linkTo(row1Barrier, margin = spaceBetweenRows)
    })
    Text(
        text = "Second line of text which will overflow into two lines of text :)",
        modifier = Modifier
            .padding(start = spaceAfterBullet)
            .constrainAs(text2) {
                start.linkTo(bulletBarrier)
                top.linkTo(row1Barrier, margin = spaceBetweenRows)
            })

    Text(text = "99.", textAlign = TextAlign.End, modifier = Modifier.constrainAs(bullet3) {
        top.linkTo(row2Barrier, margin = spaceBetweenRows)
    })
    Text(
        text = "\"Every man has two lives, and the second starts when he realizes he has just one\" — Confucius",
        modifier = Modifier
            .padding(start = spaceAfterBullet)
            .constrainAs(text3) {
                start.linkTo(bulletBarrier)
                top.linkTo(row2Barrier, margin = spaceBetweenRows)
            })

}

enter image description here

I also created a generic one in this gist here which allows for some reuse.

Oliver Metz
  • 2,712
  • 2
  • 20
  • 32
0

Currently in Jetpack Compose, this code throws an IllegalStateException because you cannot nest two vertically scrolling Composables:

This is not entirely true, what it says you can't pass Constraints.Infinity to another scrollable Composable. Composables with scroll as LazyLists also rely on scroll modifier are not allowed to measure themselves with maxWidth/Height of infinity. If you change maxHeight to something finite you won't have the issue. Constraints mean a range to measure Composables before placing them. If you pass a 0-finite dimension it will be measured in dimension.

You don't have give fix height either if your content in vertical grid is smaller than parent height- sum of other content height.

Passing it with Modifier.heighIn(max) you constrain height measurement of grid to 0 and between parent.

@Preview
@Composable
private fun LazyTest() {
    BoxWithConstraints {
        val parentHeight = maxHeight
        LazyColumn {
            item {
                Text(text = "My LazyColumn Title")
            }
            item {
                LazyVerticalGrid(
                    modifier = Modifier.heightIn(max = parentHeight),
                    columns = GridCells.Fixed(4)
                ) {
                    items(10) {
                        Box(
                            modifier = Modifier
                                .size(50.dp)
                                .padding(5.dp)
                                .background(Color.Gray)
                        )
                    }
                }
            }
        }
    }
}

If in your case other if you wish to allow Grid to cover rest of the LazyColumn height you can measure height of other elements and use Modifier.heigh(parent height - sum of height of other content), this is actually pretty easy with a custom Layout.

And if you are curious why this exception occurs it's in scroll code because they do a check before setting composable with scroll modifier as.

private class ScrollingLayoutNode(
    var scrollerState: ScrollState,
    var isReversed: Boolean,
    var isVertical: Boolean
) : LayoutModifierNode, Modifier.Node() {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // And here in LazyVerticalGrid there is a check if there is Constraints.Infinity
        checkScrollableContainerConstraints(
            constraints,
            if (isVertical) Orientation.Vertical else Orientation.Horizontal
        )

        //  this is how LazyColumn passes Constraints.Infinity content or child Composables
        val childConstraints = constraints.copy(
            maxHeight = if (isVertical) Constraints.Infinity else constraints.maxHeight,
            maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity
        )
        val placeable = measurable.measure(childConstraints)
        val width = placeable.width.coerceAtMost(constraints.maxWidth)
        val height = placeable.height.coerceAtMost(constraints.maxHeight)
        val scrollHeight = placeable.height - height
        val scrollWidth = placeable.width - width
        val side = if (isVertical) scrollHeight else scrollWidth
        // The max value must be updated before returning from the measure block so that any other
        // chained RemeasurementModifiers that try to perform scrolling based on the new
        // measurements inside onRemeasured are able to scroll to the new max based on the newly-
        // measured size.

}

And checkScrollableContainerConstraints function is

fun checkScrollableContainerConstraints(
    constraints: Constraints,
    orientation: Orientation
) {
    if (orientation == Orientation.Vertical) {
        check(constraints.maxHeight != Constraints.Infinity) {
            "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."
        }
    } else {
        check(constraints.maxWidth != Constraints.Infinity) {
            "Horizontally scrollable component was measured with an infinity maximum width " +
                "constraints, which is disallowed. One of the common reasons is nesting layouts " +
                "like LazyRow and Row(Modifier.horizontalScroll()). 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 LazyRow 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."
        }
    }
}
Thracian
  • 43,021
  • 16
  • 133
  • 222