Kes Walker
Kes Walker

Reputation: 1493

Jetpack Compose: How to put a LazyVerticalGrid inside a scrollable Column?

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.

Upvotes: 62

Views: 42914

Answers (15)

Mateusz Kaflowski
Mateusz Kaflowski

Reputation: 2367

Two versions:

A - replacing LazyVerticalGrid with chunked Rows:

Column() {
    episodes.chunked(2).forEach { rowItems ->
        Row() {
            rowItems.forEach { item ->
                Box(Modifier.weight(1f)) {
                    Item(item)
                }
            }
        }
    }
}

B - setting heightIn max suggested by @Thracian but more nasty for me:

Column() {
    LazyVerticalGrid(
        columns = GridCells.Fixed(2),
        modifier = Modifier
            .heightIn(max = 1000.dp)
    ) {
        items(items.size) { itemIndex ->
            Item(items[episodeIndex])
        }
    }
}

Upvotes: 0

Nagesh
Nagesh

Reputation: 1

If we don't want to put whole screen in lazyGrid best way i come across is using flowRow by creating custom grid layout

FlowGridLayout

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun <T> FlowGridLayout(
    modifier: Modifier = Modifier,
    items: List<T>,
    itemsInARow: Int,
    itemContent: @Composable (T) -> Unit
) {
    FlowRow(
        modifier,
        verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
        horizontalArrangement = Arrangement.SpaceBetween,
        maxItemsInEachRow = itemsInARow
    ) {
        items.forEach {
            Box(Modifier.weight(1f)){
                itemContent(it)
            }
        }
        // add empty feature to fill last row if row is not filled
        val totalItems = items.size
        val remainder = totalItems % itemsInARow
        if (remainder != 0) {
            val itemsToAdd = itemsInARow - remainder
            repeat(itemsToAdd) {
                Spacer(Modifier.weight(1f).height(0.dp)) // Add a Spacer as a placeholder
            }
        }
    }
}

Usage

FlowGridLayout(
   items = state.items,
   itemsInARow = 4,
   modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp),
   itemContent = { item ->
      // Todo Add Item Component here
    }
)

Upvotes: 0

April
April

Reputation: 61

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

Upvotes: 6

Leo Htoo
Leo Htoo

Reputation: 21

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.

Upvotes: 2

Thracian
Thracian

Reputation: 67268

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

Upvotes: 16

Erik B
Erik B

Reputation: 2858

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

Upvotes: 4

Mike
Mike

Reputation: 600

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

Upvotes: 4

nBrrr
nBrrr

Reputation: 43

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

Upvotes: 1

Harry Bloom
Harry Bloom

Reputation: 2449

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!

Upvotes: 3

Arun P M
Arun P M

Reputation: 370

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

Upvotes: 2

sgtpotatoe
sgtpotatoe

Reputation: 430

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.

Upvotes: 4

Maryam Memarzadeh
Maryam Memarzadeh

Reputation: 236

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

Upvotes: 12

Reza
Reza

Reputation: 4773

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

Upvotes: 38

Jaidyn Belbin
Jaidyn Belbin

Reputation: 585

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

Upvotes: 17

gaohomway
gaohomway

Reputation: 4080

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
}

Upvotes: 4

Related Questions