Search code examples
kotlinandroid-jetpack-composeandroid-jetpack-compose-material3android-chipslazyvgrid

Composing a Jetpack LazyGrid of FilterChip


I am trying to create a UI which looks very similar to the one at the top of the Material3 Chip page. enter image description here It appears that the best way to do so is to construct a LazyVerticalStaggeredGrid, whose content consists of FilteredChip composables.

A single FilteredChip in a Column displays just fine. But when I wrap it in a LazyVerticalStaggeredGrid instead, it is ignoring the text size constraints and reducing the width of each chip, and giving a very large height. According to Layout Inspector, the text has width 0dp and height 140dp. In addition, it's ignoring my theming color scheme.

I expected it to measure the size of the text within the Text composable, and pass that up to determine the width of each chip and the height of each row. If that's not how LazyGrids work, how should I approach this?

Here's a very simple example that illustrates my problem:

@Composable
fun VerticalGrid(
    contentList: List<String>,
    modifier: Modifier = Modifier,
) {
    LazyVerticalStaggeredGrid(
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        columns = StaggeredGridCells.Adaptive(minSize = 4.dp),
        verticalItemSpacing = 8.dp,
        modifier = modifier.fillMaxSize()
    ) {
        var selected = false
        items(contentList) { text ->
            Chip(text, selected, { selected = !selected }, modifier)
        }
    }
}

@Composable
fun Chip(
    text: String,
    selected: Boolean,
    onClick: () -> (Unit),
    modifier: Modifier = Modifier,
) {
    FilterChip(
        onClick = onClick,
        label = { Text(text = text) },
        selected = selected,
        modifier = modifier
    )
}

object Content {
    val contentList = mutableListOf<String>()

    init {
        for (i in 10..50) {
            contentList.add("Label$i")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun ChipPreview1() {
    StaggeredGridOfChipsTheme {
        Column(
            verticalArrangement = Arrangement.Top,
            modifier = Modifier.fillMaxSize()
        ) {
            Chip("Hello", true, {})
        }
    }
}

@Preview(showBackground = true)
@Composable
fun VerticalGridPreview1() {
    StaggeredGridOfChipsTheme {
        VerticalGrid(Content.contentList, Modifier.fillMaxSize())
    }
}

It results in the following behavior: enter image description here

Any tips that would have helped me troubleshoot this would also be welcome.


Solution

  • You restrict the size of each column in the LazyVerticalStaggeredGrid by using StaggeredGridCells.Adaptive(minSize = 4.dp). From the documentation:

    Defines a grid with as many rows or columns as possible on the condition that every cell has at least minSize space and all extra space distributed evenly.

    That means the columns are only 4.dp wide (+ some marginal extra space). Try setting it to 80.dp to see the difference.

    If your chips are not all of the same size a Grid isn't the best choice anyways, you should use a FlowRow instead:

    FlowRow(
        modifier = modifier,
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        contentList.forEach { text ->
            var selected by remember { mutableStateOf(false) }
            Chip(text, selected, { selected = !selected })
        }
    }
    

    Let's have a closer look at what a LazyVerticalStaggeredGrid actually is:

    1. LazyVerticalStaggeredGrid: The lazy vertical behavior is comparable to that of a LazyColumn. It means you have no vertical limitation, you can scroll endlessly (so long as there are elements available to display, of course).

    2. LazyVerticalStaggeredGrid: A grid consists of rows and columns where all cells have the same size. They don't need to be square (i.e. width==height), but each cell has the same width as all other cells. The same applies to the height. A rgid is like a raster.

    3. LazyVerticalStaggeredGrid: A staggered grid, on the other hand can have variable sized cells. Obviously the cells cannot have an arbitrary size, otherwise there would be a lot of empty, unused space. The staggering relates to the variable size in the scroll direction, only that is variable. In a vertically scrolling staggered grid that is the height.

    Look at this example from the docs:

    LazyVerticalStaggeredGrid with three fixed columns

    It has three columns of equal width that can be vertically scrolled. Each cell has the same width but a varying height. That's how a LazyVerticalStaggeredGrid typically looks like.

    That image is from the second example of the link above. It explicitly sets the number of columns to three using StaggeredGridCells.Fixed(3). The actual width of the columns is dependent on the overall width available, each column gets a third of it. When the device is rotated to landscape mode it will still be three columns, each one with a much larger width, though.

    The first example, however, uses StaggeredGridCells.Adaptive(200.dp), similar to what you used in your code. That means that the number of columns is flexible and adjusts to the available space. The only restriction is that each column must be at least 200.dp wide. So let's assume the overall width available to the LazyVerticalStaggeredGrid is 500.dp. That means only two columns with at least 200.dp can fit. The remaining 100.dp are evenly distributed to the other columns, so each one is 250.dp wide. When the device is rotated to landscape mode and there is now 1000.dp available then you would have five columns, each exactly 200.dp wide. That's why it is called Adaptive: The number of columns adapts to the space available.

    Now when the actual content of each cell is displayed (images in these examples, Chips in your code) the content is resized to fit. This is how Compose layout components work in general, it is not specific to LazyVerticalStaggeredGrid. In your code the columns are only 4.dp wide (plus a negligible fraction of the remaining space) so the Chips are requested to resize themselves accordingly. They do as best as they can, wrapping each letter on a new line so the width gets as small as possible while growing in height. That is possible because the height is not restricted (see point 3 above, the grid is vertically staggered.)


    Unrelated, but still noteworthy:

    1. You should pass the modifier parameter of VerticalGrid only to the first element (LazyVerticalStaggeredGrid), not to other elements (the Chips). Otherwise it will mess with the modifier semantics and become unpredictible for the caller.

    2. In VerticalGrid you declare the variable selected as a plain Boolean. It needs to be a MutableState<Boolean> though if you want your Chips to update accordingly. Also, that variable needs to be moved inside the items loop so every chip has its own State, otherwise toggling one chip will toggle all others as well. Compare with how I did it above in the FlowRow example.

    3. You can create your test data Content.contentList much easier. If you want a list of 41 different labels you could simply use this:

      List(41) { "Label$it" }
      

      If you want it to be from 10 to 50 inclusive, you can use this:

      List(41) { "Label${it + 10}" }