55

When trying to put a LazyVerticalGrid inside a scrollable Column I get the following error:

java.lang.IllegalStateException: Nesting scrollable in the same direction layouts like LazyColumn and Column(Modifier.verticalScroll()) is not allowed. If you want to add a header before the list of items please take a look on LazyColumn component which has a DSL api which allows to first add a header via item() function and then the list of items via items().

I am not making a traditional list, I just have alot of elements that are too big to fit on the screen. Therefore I want the column to scroll so I can see all the elements. Here is my code:

@ExperimentalFoundationApi
@Composable
fun ProfileComposable(id: String?) {
    val viewModel: ProfileViewModel = viewModel()
    if (id != null) {
        viewModel.getProfile(id)
        val profile = viewModel.profile.value
        val scrollState = rememberScrollState()
        if (profile != null) {
            Column(modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight()
                .verticalScroll(scrollState)) {
                Row() {
                    ProfilePic(profile.getImgUrl(), profile.name)
                    Column(Modifier.padding(16.dp)) {
                        ProfileName(profile.name)
                        Stats(profile.stats) //      <--------------- the offending composable
                    }
                }
                Sprites(sprites = profile.sprites)
                TextStat(profile.id.toString(), "Pokemon Number")
                TextStat(profile.species.name, "Species")
                TextStat(profile.types.joinToString { it.type.name }, "Types")
                TextStat(profile.weight.toString(), "Weight")
                TextStat(profile.forms.joinToString { it.name }, "Forms")
            }
        } else {
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                CircularProgressIndicator()
            }
        }
    } else {
        Text("Error")
    }
} 

The Stats() composable contains the LazyVerticalGrid which causes the error:

@ExperimentalFoundationApi
@Composable
fun Stats(stats: List<Stat>) {
    LazyVerticalGrid(cells = GridCells.Fixed(2)) {
        itemsIndexed(stats) { index, item ->
            StatBox(stat = item)
        }
    }
}

I do not want the grid to scroll, I just want to display a grid within a scrollable column.

5
  • 1
    Could you check if this questions helps you? stackoverflow.com/questions/66908737/…
    – nglauber
    Commented Jun 10, 2021 at 12:40
  • @nglauber unfortunately not. Wrapping it in item{} & LazyColumn doesn't work, I get the same error.
    – Kes Walker
    Commented Jun 10, 2021 at 13:01
  • @nglauber yes it does not work, have to do using column and row combination. Commented Sep 13, 2021 at 18:18
  • @KesWalker any solution was found? Commented Sep 16, 2021 at 17:06
  • @ArtemGarkusha nope.
    – Kes Walker
    Commented Sep 20, 2021 at 14:36

13 Answers 13

31

I had a similar use-case, the goal was to design a profile screen that has a bunch of information and statistics on top, and then comes to the posts as a Grid in the bottom of the screen.

I ended up using the LazyVerticalGrid for the whole list and setting full span for the items that need to fill the entire screen:

LazyVerticalGrid(cells = GridCells.Fixed(3)) {
    item(span = { GridItemSpan(3) }) { TopInfo() }
    item(span = { GridItemSpan(3) }) { SomeOtherInfo() }
    item(span = { GridItemSpan(3) }) { BottomInfo() }
    items(gridItems) { GridItemView(it) }
}
5
  • 1
    This will minimize contents of an item if it's placed below the items Commented Jan 16, 2022 at 19:10
  • 6
    maxCurrentLineSpan can be used instead of GridItemSpan(3)
    – nuhkoca
    Commented Mar 14, 2022 at 18:41
  • 1
    In this code, how can we add a full width composable at the end. Because if the items are 5 with span count 3. Then any item added at the end wouldn't come to the next line, instead will fill the adjacent space in the Grid. Commented May 2, 2022 at 5:55
  • @RahulRastogi you can use GridItemSpan(maxLineSpan) instead Commented Feb 21 at 14:47
  • We should not use GridItemSpan(maxLineSpan) -> This will take the remaining cells so let's say if we have 2*2 grid and we have total 3 items in the list than for the new list item we would expect that it will take take full width but it will not. GridItemSpan(2) -> This will force the item to take full width Commented Jul 16 at 15:58
17

I just ran into this problem myself. As @gaohomway said, your best bet is to use the experimental FlowRow() from Google's Accompanist library.

Here is a working code snippet as an example:

@Composable
fun ProfileScreen2() {
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
    ) {
        item {
            Box(modifier = Modifier.fillMaxWidth().height(200.dp).background(color = Red))
        }

        item {
            Box(modifier = Modifier.fillMaxWidth().height(200.dp).background(color = Gray))
        }

        item {
            FlowRow() {
                SampleContent()
            }
        }
    }
}

@Composable
internal fun SampleContent() {
    repeat(60) {
        Box(
            modifier = Modifier
                .size(64.dp)
                .background(Blue)
                .border(width = 1.dp, color = DarkGray),
            contentAlignment = Alignment.Center,
        ) {
            Text(it.toString())
        }
    }
}

Displays this scrollable page (don't mind the nav bar at the bottom):

1
  • 3
    FlowRow has performance issues if there is a large dataset. Commented Aug 11, 2022 at 5:47
11

try using exact height for LazyVerticalGrid, it worked for me :

@ExperimentalFoundationApi
@Composable
fun Stats(stats: List<Stat>) {
   LazyVerticalGrid(columns = GridCells.Fixed(2),
    modifier = Modifier.height(50.dp)) {
    itemsIndexed(stats) { index, item ->
        StatBox(stat = item)
    }
  }
}
3
  • 1
    You also can calculate the height dynamically using items count.
    – Psijic
    Commented Dec 4, 2022 at 10:57
  • @Psijic this looks great, but how can this be done?
    – Siele Kim
    Commented Jan 12, 2023 at 9:46
  • @SieleKim if you know your item size (or can measure it in the process), you can get a total item count (list.size) and multiply it properly. Especially if you are using GridCells.Fixed.
    – Psijic
    Commented Feb 23, 2023 at 17:33
5

This error prints now

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.

And happens because Modifier.verticalScroll changes maximum height Constraints.Infinity which also is done inside LazyLists but there is a check that maximum height is not infinite number and throws this error when you don't provide finite height bound.

Constraints is a range not a dimension assignment by changing infinity to some finite number LazyGrid or LazyColumn can be measured with 0 and the max height provided.

For instance if your content height is 210.dp and you set 1000.dp you get 210.dp height. Bottomline is setting maxHeight as finite number fixes issue without the need of calculating paddings and heights manually.

Set modifier = Modifier.heightIn(max = 1000.dp) or any number to have a LazyVerticalGrid that is measured with its content height.

enter image description here

@Preview
@Composable
private fun Test() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
    ) {
        Box(modifier = Modifier.fillMaxWidth().height(200.dp).background(Color.Red))

        Stats(
            modifier = Modifier.heightIn(max = 1000.dp),
            stats = List(8) { it }
        )

        Box(modifier = Modifier.fillMaxWidth().height(200.dp).background(Color.Yellow))

    }
}

@Composable
fun Stats(
    modifier: Modifier = Modifier,
    stats: List<Int>
) {
    LazyVerticalGrid(
        modifier = modifier,
        columns = GridCells.Fixed(2)
    ) {

        itemsIndexed(stats) { index, item ->
            Box(
                modifier = Modifier.aspectRatio(1f),
                contentAlignment = Alignment.Center
            ) {
                Text("Item $index")
            }
        }
    }
}
2
  • 1
    Thanks for mentioned the heightIn, this is the one-liner solution I'm looking for.
    – mochadwi
    Commented Feb 13 at 1:09
  • The answer worked for me perfectly, but I think that this line is a little incorrect: Set modifier = Modifier.heightIn(max = 1000.dp) or any number It's important not to set heightIn to a value such as Int.MAX_VALUE.dp, i.e infinity Commented May 25 at 23:42
5

Like Thracian's answer, but with nestedScroll,because when items count more than 8, it will default scrolling to the inner Lazy LazyVerticalGrid

@Preview(showBackground = true)
@Composable
private fun Test() {
    val outState = rememberScrollState()
    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(outState)
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp)
                .background(Color.Red)
        )

        Stats(
            modifier = Modifier
                .heightIn(max = 1000.dp)
                .nestedScroll(connection = object : NestedScrollConnection {
                    override fun onPreScroll(
                        available: Offset,
                        source: NestedScrollSource
                    ): Offset {
                        if (outState.canScrollForward && available.y < 0) {
                            val consumed = outState.dispatchRawDelta(-available.y)
                            return Offset(x = 0f, y = -consumed)
                        }
                        return Offset.Zero
                    }
                }),
            stats = List(500) { it }
        )
    }
}

@Composable
fun Stats(
    modifier: Modifier = Modifier,
    stats: List<Int>
) {
    LazyVerticalGrid(
        modifier = modifier,
        columns = GridCells.Fixed(2)
    ) {

        itemsIndexed(stats) { index, item ->
            Box(
                modifier = Modifier.aspectRatio(1f),
                contentAlignment = Alignment.Center
            ) {
                Text("Item $index")
            }
        }
    }
}
4

Reason

Nesting scrollable in the same direction layouts like LazyColumn and Column(Modifier.verticalScroll()) is not allowed.

Can't find LazyVerticalGrid forbidden scrolling temporarily

Alternatives

Alternative library from Android official Jetpack Compose Flow Layouts

FlowRow {
    // row contents
}

FlowColumn {
    // column contents
}

4

I ran into this same issue but in my case I just wanted to display 1-10 items with the same Composable component but different parameters, so I ended up creating a custom grid where you can pass:

  1. List of composables
  2. number of desired items per row
@Composable
fun VerticalGrid(composableList: List<@Composable () -> Unit>, itemsPerRow: Int) {
  val components = composableList.toMutableList()
  Column(Modifier.fillMaxWidth()) {
    while (components.isNotEmpty()) {
      val rowComponents: List<@Composable () -> Unit> = components.take(itemsPerRow)
      val listOfSpacers: List<@Composable () -> Unit> = listOfSpacers(itemsPerRow - rowComponents.size)
      RowWithItems(items = rowComponents.plus(listOfSpacers))
      components.removeAll(rowComponents)
    }
  }
}

private fun listOfSpacers(number: Int): List<@Composable () -> Unit> {
  val mutableList = emptyList<@Composable () -> Unit>().toMutableList()
  repeat(number) {
    mutableList.add { Spacer(Modifier) }
  }
  return mutableList.toList()
}

@Composable
private fun RowWithItems(items: List<@Composable () -> Unit>) {
  Row(Modifier.fillMaxWidth()) {
    items.forEach { item ->
      Box(Modifier.weight(1f)) {
        item()
      }
    }
  }
}

Example on how to call:

    VerticalGrid(
      composableList = listOf(
        { ProfileDataField(value = profileInfo.country.name) },
        { ProfileDataField(value = profileInfo.dateOfBirth) },
        { ProfileDataField(value = profileInfo.gender) }
      ),
      itemsPerRow = 2
    )

Might not be the best for performance and it's definitely not lazy but currently there is no non-lazy Grid for this purpose.

1
  • This approach is a great answer, but the mutable lists are not great IMHO.
    – Erik B
    Commented Oct 5, 2023 at 21:43
4

Like @sgtpotatoe's answer, but without the mutable lists:

/**
 * Provides an alternate to LazyVerticalGrid that allows embedding inside
 * a vertical scrollable.
 */
@Composable
fun VerticalGrid(
    composableList: List<@Composable () -> Unit>,
    itemsPerRow: Int,
    modifier: Modifier = Modifier,
) {
    Column(modifier.fillMaxWidth()) {
        composableList
            .chunked(itemsPerRow)
            .map {
                it + listOfSpacers(itemsPerRow - it.size)
            }
            .forEach { rowComponents ->
                Row(Modifier.fillMaxWidth()) {
                    rowComponents.forEach { item ->
                        Box(Modifier.weight(1f)) {
                            item()
                        }
                    }
                }
            }
    }
}

private fun listOfSpacers(count: Int): List<@Composable () -> Unit> =
    List(count) { { Spacer(Modifier) } }
1
  • This worked like a charm and saved me much headaches. Commented Oct 26, 2023 at 15:09
3

I got around this issue by setting a dynamically generated height value to the inner nested LazyVerticalGrid. I'll break it down.

In the LazyVerticalGrid view, set the follow constants needed to work out the height (i'm using headers to separate sections in my vertical grid):

val headerHeight = 50
val gridCardWidth = 100
val gridCardHeight = 136
val adaptiveCellType = GridCells.Fixed(3)

Then define this function to work out the height of your content:

fun getGridHeight(): Int {
    val padding = 24
    var runningHeight = 0
    categories.forEach {
        val cardRowHeight = ((max(1, (it.items.size / 3))) * gridCardHeight)
        runningHeight += headerHeight + padding + cardRowHeight
    }
    return runningHeight
}

Now you can set this height value to the LazyVerticalGrid view modifier, and it will define the scrollable height on load, and then the error goes away.

LazyVerticalGrid(
    modifier = Modifier
        .height(getGridHeight().dp)

It's a bit of a hack, but it works for now!

3

If you don't want to use LazyVerticalGrid because of the nested scroll issues it causes inside other scrollable/lazy components, I created a simple GridView solution:

@Composable
fun SimpleGridView(
    modifier: Modifier = Modifier,
    columns: Int,
    countOfItems: Int,
    content: @Composable() (index: Int) -> Unit
) {
    val columnAndRowItems = (0..countOfItems).chunked(columns)

    Column(modifier = modifier) {
        columnAndRowItems.forEach { rowItems ->
            Row(modifier = Modifier.fillMaxWidth()) {
                rowItems.forEach { index ->
                    Box(modifier = Modifier.weight(1f)) {
                        content(index)
                    }
                }
            }
        }
    }
}

Here is an example of how to use it:

val listOfItems = listOf("one", "two", "three", "four")
SimpleGridView(
    modifier = Modifier.fillMaxWidth(),
    columns = 2,
    countOfItems = listOfItems.size,
) { index ->
    Text(text = listOfItems[index])
}
2
  • This will look weird if columnCount is 3 and the total items is 5
    – Erik B
    Commented Oct 5, 2023 at 21:48
  • @ErikB One of the solutions is that you can try to set the count of items to 6 and use listOfItems.getOrNull(index)?.let { Text(text = it) } inside.
    – Mike
    Commented Nov 6, 2023 at 15:28
2

I think FlowRow is not suitable in cases where we need to specify the grid count (or column count) in a row. So as mentioned some other answers I ended up creating a custom VerticalGrid which is not a lazy one (yeah..performance issue is there..but it works). Here is my solution.

@Composable
fun VerticalGrid(columnCount: Int, items: () -> List<(@Composable () -> Unit)>) {
    Column {
        items.invoke().windowed(columnCount, columnCount, true).forEach {
            Row(horizontalArrangement = Arrangement.SpaceBetween) {
               it.forEach { Box(modifier = Modifier.weight(1f)) { it.invoke() } }
            }
        }
    }
}

And you can use this inside a LazyColumn or Column like this

LazyColumn() {
  items(mainListData) { listData ->
      ....

      VerticalGrid(columnCount = 3) {
          listData.gridData.map {
              {
                 //add your composable, as an example adding a Text
                 Text(uiData.anyValue)
              }
          }
      }

     ....
   }
}

Grid rendering sample

4
  • .chunked(columnCount) works identically to your use of windowed.
    – Erik B
    Commented Oct 5, 2023 at 21:47
  • This will look weird if columnCount is 3 and the total items is 5
    – Erik B
    Commented Oct 5, 2023 at 21:47
  • could you please share a screenshot link @Erik B?
    – Arun P M
    Commented Oct 9, 2023 at 15:04
  • I should have said: a row having 3 items with equal weight will not line up with a row having 2 items with equal weight. Some of the other solutions add a Spacer to make the items in the row match the columnCount
    – Erik B
    Commented Oct 9, 2023 at 19:52
2

You can write Custom Grid Composable using normal Row and Column components without Lazy effect. It might have performance issue for massive dataset but it works.
See code snippet below :

@Composable
fun <T> NonLazyVerticalGrid(
  modifier: Modifier = Modifier,
  columns: Int,
  data: List<T>,
  verticalSpacing: Dp = 0.dp,
  horizontalSpacing: Dp = 0.dp,
  itemContent: @Composable (item: T) -> Unit
) {

  Box(modifier = modifier) {

    Column(
      modifier = Modifier
        .fillMaxWidth()
    ) {

      val numOfRows = (data.size / columns) + (if (data.size % columns > 0) 1 else 0)

      repeat(numOfRows) { i ->

        Row(
          modifier = Modifier
            .fillMaxWidth(),
          horizontalArrangement = Arrangement.spacedBy(horizontalSpacing)
        ) {

          repeat(columns) { j ->

            val index = j + (i * columns)

            if (index < data.size) {
              Column(
                modifier = Modifier
                  .weight(1f),
                horizontalAlignment = Alignment.CenterHorizontally
              ) {
                itemContent(data[index])
              }
            } else {
              Box(modifier = Modifier.weight(1f))
            }
          }
        }

        Spacer(modifier = Modifier.height(verticalSpacing))

      }
    }

  }

}

Example Usage:

    NonLazyVerticalGrid(
      columns = 4,
      data = itemList
    ) { item ->
      //your composable item here
      Text(text="${item.name}")
    }

It also provides for vertical spacing and horizontal spacing between items.
1

I had the same issue because I just changed Column() to LazyColumn() and kept modifier = Modifier.verticalScroll(scrollState) in the LazyColumn. So check out your LazyColumn modifier for vertical or horizontal scroll and just delete it before you change anything else

Not the answer you're looking for? Browse other questions tagged or ask your own question.