Reputation: 1493
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
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
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
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
}
}
}
}
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
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
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}")
}
Upvotes: 2
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.
@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
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
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
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
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
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)
}
}
}
....
}
}
Upvotes: 2
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:
@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
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
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
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
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