Skip to content

astamato/ContextualFlowRowSample

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

9 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

This is a Kotlin Multiplatform project targeting Android, iOS.

  • /composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:

    • commonMain is for code that’s common for all targets.
    • Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, iosMain would be the right folder for such calls.
  • /iosApp contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.

Learn more about Kotlin Multiplatform

ContextualFlowRow Sample

A Jetpack Compose implementation of a contextual flow layout with overflow handling, built as a replacement for the deprecated ContextualFlowRow component. This sample demonstrates how to create a smart, responsive layout that automatically shows a "X+ more" chip when content overflows.

πŸ“– Caveat

This solution uses Layout and composes all list items, irrespective of whether they'll be displayed or not. Because this implementation does not rely on SubComposeLayout, it becomes more performant for shorter lists, which is typically the use case for chip arrangements like the one portrayed in the example. As the list grow the more we could've benefited from using an implementation that leverages SubComposeLayout to avoid composing all items straight away.

🚨 Why this exists: With the deprecation of official flow components, this sample provides a production-ready alternative with enhanced overflow management.

✨ Features

  • 🎯 Smart Overflow Detection - Automatically detects when items don't fit
  • πŸ“± Responsive Layout - Adapts to different screen sizes and orientations
  • πŸ”’ Dynamic Counting - Shows exact number of hidden items
  • πŸ“‹ Bottom Sheet Integration - Expandable view for remaining items
  • 🎨 Customizable Styling - Flexible theming and spacing options
  • 🧩 Decoupled Architecture - Reusable components with clean separation

πŸ“± Demo

Main View Bottom Sheet
Main View Bottom Sheet

Sample showing programming languages with overflow handling

πŸ›  Implementation Overview

Core Algorithm

The ContextualFlowRow uses a custom Layout composable that implements a sophisticated measurement algorithm:

@Composable
fun <T> ContextualFlowRow(
    items: List<T>,
    maxLines: Int = Int.MAX_VALUE,
    onMoreClick: (List<T>) -> Unit = {},
    itemContent: @Composable (T) -> Unit,
)

Key Algorithmic Components

1. Template Chip Measurement

// Create a template "99+ more" chip for width calculation
AssistChip(
    onClick = { },
    label = { Text("99+ more") },
    // ... styling
)

The algorithm pre-measures a template chip to reserve space for the overflow indicator, ensuring consistent layout behavior.

2. Layout Calculation Engine

private fun <T> calculateLayout(
    items: List<T>,
    placeables: List<Placeable>,
    moreChipWidth: Int,
    maxWidth: Int,
    maxLines: Int,
    spacing: Int,
    verticalSpacing: Int,
): LayoutResult

The algorithm works in phases:

Phase 1: Greedy Item Placement

  • Places items left-to-right, top-to-bottom
  • Tracks current position (currentX, currentY)
  • Moves to next line when width exceeded

Phase 2: Overflow Detection

  • When reaching maxLines, checks if more items exist
  • Calculates space needed for "X+ more" chip
  • Removes items from current line until chip fits

Phase 3: Smart Backtracking

// If "more" chip doesn't fit, remove items from current line
while (currentX + moreChipWidth > maxWidth && lineItems.isNotEmpty()) {
    val removedItem = lineItems.removeLast()
    currentX -= (removedItem.width + spacing)
    removedFromLine++
}

3. Dynamic Count Calculation

val remainingCount = maxOf(0, items.size - visibleItemsCountRef.intValue)
Text("$remainingCount+ more")

πŸ— Architecture

Component Separation

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   ContextualFlowRow │───▢│ RemainingItemsModal │───▢│ RemainingItemsModalBottomSheet- β”‚
β”‚                     β”‚    β”‚   BottomSheet       β”‚    β”‚            Content              β”‚
β”‚ β€’ Layout Logic      β”‚    β”‚ β€’ Modal Management  β”‚    β”‚ β€’ Content Display               β”‚
β”‚ β€’ Overflow Detectionβ”‚    β”‚ β€’ State Handling    β”‚    β”‚ β€’ Item Rendering                β”‚
β”‚ β€’ Chip Measurement  β”‚    β”‚ β€’ Sheet Lifecycle   β”‚    β”‚ β€’ Flow Layout                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Design Decisions

  1. Template-Based Measurement: Pre-measures overflow chip for consistent spacing
  2. Ref-Based Counting: Uses mutableIntStateOf to avoid recomposition loops
  3. Callback-Driven: Parent manages bottom sheet state for flexibility
  4. Generic Implementation: Works with any data type <T>

πŸš€ Usage

Basic Implementation

@Composable
fun ProgrammingLanguageScreen() {
    var showBottomSheet by remember { mutableStateOf(false) }
    var remainingItems by remember { mutableStateOf<List<ProgrammingLanguage>>(emptyList()) }

    ContextualFlowRow(
        items = programmingLanguages,
        maxLines = 2,
        onMoreClick = { remaining ->
            remainingItems = remaining
            showBottomSheet = true
        }
    ) { language ->
        FilterChip(
            onClick = { /* handle selection */ },
            label = { Text(language.name) }
        )
    }

    RemainingItemsModalBottomSheet(
        items = remainingItems,
        isVisible = showBottomSheet,
        onDismiss = { showBottomSheet = false }
    ) { language ->
        FilterChip(/* same chip styling */)
    }
}

Customization Options

ContextualFlowRow(
    items = items,
    maxLines = 3,                    // Allow up to 3 rows
    horizontalSpacing = 12.dp,       // Space between items
    verticalSpacing = 8.dp,          // Space between rows
    onMoreClick = { remaining -> ... }
) { item ->
    // Custom item content
}

πŸ”§ Technical Highlights

Performance Optimizations

  • Single-Pass Layout: Measures all items once, calculates layout in one pass
  • Efficient Recomposition: Uses refs to minimize state-driven recompositions
  • Memory Efficient: Only stores positions and counts, not full item lists

Edge Case Handling

  • Empty Lists: Gracefully handles empty item collections
  • Single Item: Works correctly with just one item
  • Narrow Screens: Adapts to very small widths
  • Long Text: Handles varying item widths elegantly

Accessibility

  • Semantic Actions: Proper click handling for screen readers
  • Content Descriptions: Clear labeling of overflow actions
  • Navigation Support: Full keyboard and D-pad navigation

πŸ“š Key Learnings

Layout Measurement Insights

  1. Pre-measuring Template Components prevents layout shifts
  2. Backtracking Algorithms ensure consistent visual balance
  3. State Management Outside Layout avoids infinite recomposition
  4. Generic Type Safety enables reuse across different data types

Compose Layout Patterns

Layout(
    modifier = modifier,
    content = {
        // All possible content (items + template)
    }
) { measurables, constraints ->
    // Measure once, place optimally
    val result = calculateLayout(...)

    layout(result.totalWidth, result.totalHeight) {
        // Place all items at calculated positions
    }
}

🎯 Use Cases

  • Tag/Chip Lists: Skills, categories, filters
  • Navigation Menus: Breadcrumbs, tabs, quick actions
  • Content Previews: Image galleries, article lists
  • Selection Components: Multi-select dropdowns, choice chips

🀝 Contributing

This is a sample implementation. Feel free to:

  • πŸ› Report issues or edge cases
  • πŸ’‘ Suggest algorithmic improvements
  • 🎨 Contribute styling enhancements
  • πŸ“– Improve documentation

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

Feel free to use this implementation in your projects!


πŸ’‘ Pro Tip: This implementation showcases advanced Compose Layout techniques. Study the calculateLayout function to understand custom layout algorithms in Jetpack Compose!

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published