Dynamic Layouts with ContextualFlowRow and ContextualFlowColumn
At Google I/O 2024, a new layout component for Compose was announced, the "Contextual Flow Layout", including both a ContextualFlowRow
and ContextualFlowColumn
. These new components allow for layouts that show a fixed number or rows or columns respectively while filling the allocated space. This has major advantages for creating consistent layouts for dynamic lists without a ton of work, but also makes screens better adapt to large form factor devices like tablets and foldables.
A Note on FlowRow and FlowColumn #
FlowRow
and FlowColumn
also received updates with similar parameters including maxLines
and overflow
, however there are some key differences. For one the contextual layouts provide an index and other scoped values for rendering elements for the content parameter that are not available to the base flow layouts. Additionally, there are stricter requirements around when you access counts in the overflow context. More importantly, unlike the contextual flow layouts, they are not lazy layouts. For this article, the focus will remain on the contextual flow layouts and the examples may not be transferable to use with FlowRow
or FlowColumn
even if the parameters are similar.
ContextualFlowRow & ContextualFlowColumn #
The API for ContextualFlowRow
and ContextualFlowColumn
are generally the, same so going forward the examples will show ContextualFlowRow
unless there is something to highlight specifically with ContextualFlowColumn
. To begin, here is a simple usage of a ContextualFlowRow
.
// Values Provided as Example
val items = List(100) { Random.nextFloat() }
ContextualFlowRow(
itemCount = items.size,
maxLines = 3,
maxItemsInEachRow = Int.MAX_VALUE,
horizontalArrangement = Arrangement.Start,
verticalArrangement = Arrangement.Top,
overflow = ContextualFlowRowOverflow.Clip,
modifier = Modifier,
) { index ->
Text(text = items[index].toString())
}
Parameters #
First is itemCount: Int
which is the only required parameter besides content
. This parameter tells the layout how many items are in the list which allows the component to know if there are more items to layout and enables lazily laying out the elements.
Next is maxLines: Int
which tells the component how many rows to layout before applying the overflow policy (more on that soon). By default the value is Int.MAX_VALUE
which would effectively mean never apply the overflow policy. This parameter is the same in ContextualFlowColumn
but applies to the number of columns instead of rows. This parameter is unaware of the size of the line and will keep laying out components so long as it has space. If the specific number of elements in a line should be restricted, use the maxItemsInEachRow: Int
to do so. This parameter is defaulted to Int.MAX_VALUE
, which is effectively no limit.
There are two parameters for arranging components within the Contextual Flow Layout, horizontalArrangement: Arrangement
and verticalArrangement: Arrangement
. These work similar to Column
and Row
arrangement parameters. For both ContextualFlowRow
and ContextualFlowColumn
the default values are Arrangement.Start
for horizontalArrangement
and Arrangement.Top
for verticalArrangement
.
The last three parameters are the standard modifier: Modifier
parameter, the overflow: ContextualFlowRowOverflow
which applies an overflow policy (overflow: ContextualFlowColumnOverflow
for ContextualFlowColumn
), and the content: @Composable ContextualFlowRowScope.(index: Int) -> Unit
block (content: @Composable ContextualFlowColumnScope.(index: Int) -> Unit
for ContextualFlowColumn
). The Content Block and Overflow Policy are the bread and butter of the contextual flow layouts and will be covered in their own sections below.
Content Block #
Contextual flow layouts are lazy layouts so elements not visible are not in the composition. To assist in this, the content
block of the contextual flow layouts provides an index
which can be used to look up the current item and then render a composable for the current item. The content is also in a ContextualFlowRowScope
which gives access to a few more values that can be helpful in more complex layouts.
- maxHeight - The maximum vertical space allowed for the element in the given row (
maxWidth
inContextualFlowColumn
) - maxWidthInLine - The maximum horizontal space available for the element in the current row (
maxHeightInLine
inContextualFlowColumn
) - lineIndex - The index of the line the element is being placed in (e.g. 1 for the second row)
- indexInLine - The index of the element in the line (e.g. 3 for the fourth element in the row)
Together, these can be used to create complex and dynamic layouts within the content block. Below is a simple example that displays values in a checkerboard style pattern.
ContextualFlowRow(
itemCount = 64,
maxItemsInEachRow = 8,
modifier = Modifier.padding(innerPadding)
) { index ->
val color = when(lineIndex % 2 == 0) {
true -> if(indexInLine % 2 == 0) {
Color.Black
} else {
Color.Red
}
false -> if(indexInLine % 2 == 0) {
Color.Red
} else {
Color.Black
}
}
Box(modifier = Modifier.size(48.dp).background(color)) {
Text(text = index.toString(), color = Color.White)
}
}
Overflow Policy #
There are four overflow policies provided to use and they are Clip
, Visible
, expandIndicator
, and expandOrCollapseIndicator
.
Clip #
This policy hides all elements that don't fit within the maxLines
. This is the default setting for ContextualFlowRow
and ContextualFlowColumn
.
overflow = ContextualFlowRowOverflow.Clip
Visible #
This policy shows all elements, even those that don't fit within the maxLines
.
overflow = ContextualFlowRowOverflow.Visible
expandIndicator #
This policy hides all the elements and shows the provided content
if there are hidden elements. This content
block which is provided to the expandIndicator
is in the context of ContextualFlowRowOverflowScope
. This scope provides the shownItemCount
and totalItemCount
. These can be used to provide information to the user about the displayed elements including the number of hidden elements (totalItemCount - shownItemCount
). The content
block is a generic composable so it is up to the consumer to provide appropriate UI and functionality.
overflow = ContextualFlowRowOverflow.expandIndicator {
val hiddenCount = totalItemCount - shownItemCount
Button(onClick = { /* TODO: Show Items */ }) {
Text(text = "Show $hiddenCount Hidden Item(s)")
}
}
expandOrCollapseIndicator #
This policy build upon by the expandIndicator
policy by adding an additional parameter for a collapse indicator that shows when all elements are visible. Like the expandIndicator
policy, both the expandIndicator
and collapseIndicator
parameters are in the ContextualFlowRowOverflowScope
. In addition to the expandIndicator
and collapseIndicator
parameters, this policy also takes a minRowsToShowCollapse
for specifying the minimum visible lines before collapse is shown and minHeightToShowCollapse
for specifying a minimum space available before showing the collapseIndicator
(minColumnsToShowCollapse
and minWidthToShowCollapse
respectively for ContextualFlowColumn
) The intent of this is to allow expanding to show all, and collapsing back to the previous state. Again like the expandIndicator
, the
overflow = ContextualFlowRowOverflow
.expandOrCollapseIndicator(
expandIndicator = {
val hiddenCount = totalItemCount - shownItemCount
Button(onClick = { /* TODO: Show Items */ }) {
Text(text = "Show $hiddenCount Hidden Item(s)")
}
},
collapseIndicator = {
Button(onClick = { /* TODO: Show Less */ }) {
Text(text = "Show Less")
}
},
)
Conclusion #
A demonstration app using the new components can be found here on GitHub for exploration. Contextual flow layouts open up a whole new set of opportunities for developing dynamic layouts. Until next time, thanks!
Did you find this content helpful?
Please share this post and be sure to subscribe to the RSS feed to be notified of all future articles!
Want to go above and beyond? Help me out by sending me $1 on Ko-fi. It goes a long way in helping run this site and keeping it advertisement free. Thank you in advance!