How to Use ContextualFlowRow with custom overflow logic | by Stevan Milovanovic | Sep, 2024

ContextualFlowRow

In the latest stable release of the Compose Foundation Layout library — release 1.7.0 from September 4, 2024, the ContextualFlowLayout is introduced with its two composables ContextualFlowRow and ContextualFlowColumn.

These are enhanced versions of FlowRow and FlowColumn since they allow you to lazy load the contents of your flow row or column.

In Jetpack Compose, FlowRow and ContextualFlowRow are both used to arrange items in a horizontal manner that automatically wraps to a new line when there’s not enough space. They lay out their children based on their intrinsic sizes, which means they try to fit as many items as possible in the available width before wrapping. However, they have some key differences in behaviour and use cases which we will try to explain.

Differences between FlowRow and ContextualFlowRow

FlowRow arranges its children in a single row that wraps to subsequent lines when necessary based on available width. On the other side, ContextualFlowRow behaves similarly to FlowRow but is designed to take into consideration the context of an item when laying out its children. This is especially useful in scenarios where the layout might depend on the current state or certain conditions, allowing for a more dynamic arrangement of the items. It also might include features that allow item arrangement based on contextual cues or extra parameters that can influence how items are laid out in real-time.

Sample Implementation

Tasty Recipe Tags as ContextualFlowRow

Recently, I created the Tasty App and explained how I did it, from idea to implementation. In this app, users can explore a variety of recipes. For each recipe, you can view detailed instructions either through video content or text descriptions. Additionally, to better understand each recipe, you can read a summary or review relevant keywords and tags.

For displaying recipe tags, I opted for a ContextualFlowRow. Here’s why: The number of tags associated with a recipe can range from just a few to over forty. Since each tag is clickable and enables users to filter recipes by that specific tag, I wanted to ensure that all tags are visible without occupying too much screen space initially. To achieve this, I decided to display only the tags that fit within the first three rows and provide users with the option to view additional tags if they choose to. This required implementing a custom overflow solution.

ContextualFlowRowOverflow has two predefined implementations:

  • ContextualFlowRowOverflow.Visible which displays all content, even if there is not enough space in the specified bounds.
  • ContextualFlowRowOverflow.Clip which clips the overflowing content to fix its container.

In addition to these two, you can implement your own handling of the content that exceeds the boundaries of its container. For this, you also have two options:

fun expandIndicator(
content: @Composable ContextualFlowRowOverflowScope.() -> Unit
)

With expandIndicator you can implement custom composable which will is going to be displayed when there are more items than it can be displayed, or you can use expandOrCollapseIndicator:

fun expandOrCollapseIndicator(
expandIndicator: @Composable ContextualFlowRowOverflowScope.() -> Unit,
collapseIndicator: @Composable ContextualFlowRowOverflowScope.() -> Unit,
minRowsToShowCollapse: Int = 1,
minHeightToShowCollapse: Dp = 0.dp,
)

As documentation states:
This function is designed to switch between two states: a “Expandable” state when there are more items to show, and a “Collapsable” state when all items are being displayed and can be collapsed.

This is how I implemented Recipe Tags Composable:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RecipeTags(
tags: List<Tag>,
modifier: Modifier = Modifier,
onTagSelected: (Int) -> Unit
) {
var overflowSelected by remember { mutableStateOf(false) }
val expandIndicator = remember {
ContextualFlowRowOverflow.expandIndicator {
val remainingItems = tags.size - shownItemCount
OverflowItem(
remainingItemsCount = remainingItems,
onOverflowItemSelected = { overflowSelected = true }
)
}
}

ContextualFlowRow(
itemCount = tags.size,
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
maxLines = 3,
overflow = if (overflowSelected) ContextualFlowRowOverflow.Visible else expandIndicator
) { index ->
TagItem(
tag = tags[index],
onTagSelected = onTagSelected
)
}
}

Where TagItem is:

@Composable
private fun TagItem(
tag: Tag,
onTagSelected: (Int) -> Unit
) {
Text(
text = "#" + tag.displayName,
modifier = Modifier
.clickable{ onTagSelected(tag.tagId) },
color = MaterialTheme.colorScheme.tertiary,
style = MaterialTheme.typography.bodySmall,
)
}

And OverflowItem is:

@Composable
private fun OverflowItem(
remainingItemsCount: Int,
onOverflowItemSelected: () -> Unit
) {
Text(
text = stringResource(R.string.show_n_more_items, remainingItemsCount),
modifier = Modifier
.clickable(onOverflowItemSelected),
color = MaterialTheme.colorScheme.tertiary,
style = MaterialTheme.typography.bodySmall,
)
}

And here is how it looks like:

Summary

In summary, while both FlowRow and ContextualFlowRow serve to lay out items in a flowing manner, ContextualFlowRow adds additional context-sensitive features that can help in creating more dynamic interfaces that adapt based on state or context. Use FlowRow for simpler, static arrangements and ContextualFlowRow for more context-aware layouts.

You can find the full source code on GitHub. I’d love to hear your thoughts and suggestions. Thank you for reading, and until next time, stay curious! 😊

Leave a Reply