42

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.

Kes Walker
  • 1,154
  • 2
  • 10
  • 24

8 Answers8

22

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) }
}
Rez
  • 4,501
  • 1
  • 30
  • 27
  • 1
    This will minimize contents of an item if it's placed below the items – MoeinDeveloper Jan 16 '22 at 19:10
  • 5
    `maxCurrentLineSpan` can be used instead of `GridItemSpan(3)` – nuhkoca Mar 14 '22 at 18:41
  • 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. – Rahul Rastogi May 02 '22 at 05:55
12

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):

Jaidyn Belbin
  • 475
  • 2
  • 7
  • 17
6

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)
    }
  }
}
  • 1
    You also can calculate the height dynamically using items count. – Psijic Dec 04 '22 at 10:57
  • @Psijic this looks great, but how can this be done? – Siele Kim Jan 12 '23 at 09: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 Feb 23 '23 at 17:33
2

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
}

gaohomway
  • 2,132
  • 1
  • 20
  • 37
1

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.

sgtpotatoe
  • 340
  • 6
  • 17
1

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!

Harry Bloom
  • 2,359
  • 25
  • 17
0

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

Arun P M
  • 352
  • 1
  • 15
0

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

nBrrr
  • 3
  • 3