Skip to main content
Donovan LaDuke - Developer

Dynamic Layouts with ContextualFlowRow and ContextualFlowColumn


Featured in Android Weekly Issue 626 As Seen In - jetc.dev Newsletter Issue #218 Featured in Android Sweets {{ androidSweets | readableDate }}

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.

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!

Buy me a Coffee on Ko-fi