Issue
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
:
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 item
s 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.
Solution
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")
}
}
}
Answered By - Adrian K
Answer Checked By - Dawn Plyler (JavaFixing Volunteer)