3

In the Android View system with a RecyclerView, one could have the GridLayoutManager decide at runtime the number of "Spans" each item used by using a callback called spanSizeLookup:

Imagine you have

val layoutManager = GridLayoutManager(this, 2) //Two Spans Max

layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
       override fun getSpanSize(position: Int): Int {
            // calculate based on whatever you want and return N
            return if (adapter.getItem(position).xx = YY) 1 else 2 //e.g.
       }
}

Now I'm attempting to convert this to Compose using LazyVerticalGrid where there's no Adapter, and certainly no LayoutManager to deal with. However, I'm having hard time finding the equivalent to the spanSizeLookup.

Initial Option: using the "DSL"

Imagine a @Composable that receives the "data" and does something like:

LazyVerticalGrid(columns = GridCells.Fixed(2)) {

   items(data) { anItem ->
       // some composable to show "anItem"
   }
}

This is fine, it will display each item in a "two" column (spans!) layout; what if you want to dynamically change the span for each item?

No problem, the items function in the DSL actually does take a span:

items(data, span = { // put your span here }) { anItem ->
       // some composable to show "anItem"
}

This span is going to apply to all items and you cannot change it... so this doesn't solve the problem.

The function block for span is expecting a GridItemSpan(Int).

So the 1st question is: Would there be a way to change this from inside the content block? Think of something like this:

items(data) { anItem ->
    // Lookup the correct span for this `item`
    span = if (anItem.xx = YY) 1 else 2
    // some composable to show "anItem"
}

This is obviously not possible like that...

The Alternative

What one CAN do, is create individual items which also accept a span and only apply to the actual item:

// Manually iterate all items
data.forEach { anItem ->
    if (anItem.xx = YY) {
        // Render the item with 1 span.
        item(span = { GridItemSpan(1) }) {
           // some composable to show...
        }
    } else {
        // Render the item with 2 spans.
        item(span = { GridItemSpan(1) }) {
           // some composable to show...
        }
    }
}
    

This works (I've tested it) but it feels a bit convoluted.

Question 2 is then: Is this "ok" according to the current (1.3.0-alpha01) version of Compose? Is there a better way?

Please keep in mind I tried to abstract the irrelevant parts of all this, so it's a bit of pseudo-code here and there to illustrate a point.

I have seen this post and similar ones, but I'm not sure if that's the right approach either, seems like a lot of complexity for something the framework APIs could better handle. I'd love to hear more about it though.

I've naturally read the official documentation to no avail.

Martin Marconcini
  • 26,875
  • 19
  • 106
  • 144

2 Answers2

3

The documentation on this is a bit scarce, that is true.
What you might have missed is: while you can only provide one span lambda, it will be called for each item, with that item as argument. This means you are free to return different span sizes depending on the argument provided.
So the implementation path is almost identical to the classic SpanSizeLookup, with the advantage that you don't have to look up items by their index (but can still opt to do it):

// Just for visualization purposes
@Composable
fun GridItem(label: String) {
    Box(
        Modifier
            .fillMaxWidth()
            .height(56.dp)
            .border(1.dp, Color.Gray, RoundedCornerShape(16.dp)),
        contentAlignment = Alignment.Center
    ) {
        Text(text = label)
    }
}

@Preview
@Composable
fun GridSpansSample() {
    LazyVerticalGrid(columns = GridCells.Fixed(3), modifier = Modifier.fillMaxSize()) {

        // based on index
        items(3, span = { index ->
            val spanCount = if (index == 0) 3 else 1
            GridItemSpan(spanCount)
        }) { index ->
            GridItem("Item #$index")
        }

        // based  on list content
        items(listOf("Foo", "Bar", "Baz"), span = { item ->
            val spanCount = if (item == "Foo") 3 else 1
            GridItemSpan(spanCount)
        }) { item ->
            GridItem(item)
        }

        // based on either content or index
        itemsIndexed(listOf("Foo", "Bar", "Baz"), span = { index, item ->
            val spanCount = if (item == "Foo" || index == 1) 3 else 1
            GridItemSpan(spanCount)
        }) { index, item ->
            GridItem(item)
        }

        // Bonus: The span lambda receives additional information as "this" context, which allows for further customization
        items(10 , span = {
            // occupy the available remaining width in the current row, but at most 2 cells wide
            GridItemSpan(this.maxCurrentLineSpan.coerceAtMost(2))
        }) { index ->
            GridItem("Item #$index")
        }
    }
}
Adrian K
  • 3,942
  • 12
  • 15
0

In my case I needed to place a whole element at the bottom of the list, let's say the list is odd with 5 elements I would place 2 elements per column and then 1 at the bottom

 LazyVerticalGrid(
        modifier = Modifier
            .fillMaxSize()
            .background(CoffeeFoam),
        columns = GridCells.Fixed(2),
        verticalArrangement = Arrangement.spacedBy(12.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {

    itemsIndexed(items = myItemList, span = { index, _ ->
                val spanCount = if (myItemList.size % 2 == 0) 1 else if (index == myItemList.size - 1) 2 else 1
                GridItemSpan(spanCount)
            }) { index, uiState ->

                CheckoutSection(
                    modifier = Modifier.wrapContentHeight(),
                    uiState = uiState,
                    onCardCtaClick = onCardCtaClick
                )
            }
}

Doing this I'm checking if the list is pair, if so I return 1 span sice (which will take 2 items per column) , then I check for the indexes and the list size if the list size is not pair, which could be 3 , 5 , 7, 9 ....

index = 0 != 4 return 1 as span size
index = 1 != 4 return 1 as span size
index = 2 != 4 return 1 as span size
index = 3 != 4 return 1 as span size
index = 4 == 4 return 2 as span size (full width element)

And in this way I'm rendering the last element with full column width on odd list sizes, we can make an exception for first element also with just one more check for index != 0

Gastón Saillén
  • 12,319
  • 5
  • 67
  • 77