androidandroid-jetpack-composecomposablelazycolumn

Fixed Grid inside LazyColumn in Jetpack Compose?


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?


Solution

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