Flow Layouts in Jetpack Compose: Complete Guide to Dynamic UI Arrangements


Introduction: Why Flow Layouts Transform Your Android UI

Struggling with displaying variable-length content elegantly in your Android app? Flow Layouts in Jetpack Compose solve this challenge by automatically arranging UI elements based on available space. Instead of forcing content into rigid rows or columns, Flow Layouts adapt dynamically - perfect for tags, filters, or any content with unpredictable dimensions.

This comprehensive guide covers everything you need to master FlowRow and FlowColumn in Compose, from basic implementation to advanced responsive techniques that will make your Android apps more flexible and user-friendly.

What Are Flow Layouts in Jetpack Compose?

Flow Layouts are adaptive layout composables that automatically arrange child elements based on available screen space. When elements don't fit in the current row or column, they flow to the next line seamlessly.

Key Components:

  • FlowRow: Arranges elements in horizontal rows, wrapping to new rows when needed
  • FlowColumn: Organizes elements in vertical columns, creating new columns as space allows

Core Benefits:

  • Responsive design without manual calculations
  • Content-aware arrangement that adapts to text length
  • Zero overflow issues with dynamic content
  • Consistent spacing across all screen sizes

Getting Started: Basic FlowRow Implementation

Essential Setup:

import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun BasicFlowRowExample() {
    val technologies = listOf(
        "Kotlin", "Android Development", "Jetpack Compose", 
        "Material Design", "MVVM", "Clean Architecture"
    )
    
    FlowRow(
        modifier = Modifier.padding(16.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        technologies.forEach { tech ->
            AssistChip(
                onClick = { /* Handle selection */ },
                label = { Text(tech) }
            )
        }
    }
}

Code Breakdown:

  • @OptIn(ExperimentalLayoutApi::class): Required annotation since Flow Layouts are experimental
  • horizontalArrangement: Controls spacing between elements in the same row
  • verticalArrangement: Manages spacing between different rows
  • Modifier.padding(): Provides outer spacing for the entire layout

FlowColumn: Vertical Flow for Wide Screens

Perfect for tablet layouts or desktop-style interfaces:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun NewsFlowColumn() {
    val newsItems = (1..12).map { "Breaking News $it: Important update that might be long" }
    
    FlowColumn(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        maxItemsInEachColumn = 4
    ) {
        newsItems.forEach { news ->
            Card(
                modifier = Modifier
                    .width(250.dp)
                    .wrapContentHeight()
            ) {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text(
                        text = news,
                        style = MaterialTheme.typography.bodyMedium,
                        maxLines = 3,
                        overflow = TextOverflow.Ellipsis
                    )
                    Spacer(modifier = Modifier.height(8.dp))
                    Button(
                        onClick = { },
                        modifier = Modifier.fillMaxWidth()
                    ) {
                        Text("Read More")
                    }
                }
            }
        }
    }
}

Key Parameters Explained:

  • maxItemsInEachColumn: Limits elements per column for better distribution
  • width(250.dp): Fixed width ensures consistent column appearance
  • wrapContentHeight(): Allows natural height adjustment

Advanced State Management with Flow Layouts

Interactive Tag Selection System:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun InteractiveSkillsSelector() {
    var selectedSkills by remember { mutableStateOf(setOf<String>()) }
    val availableSkills = listOf(
        "Kotlin", "Java", "Swift", "Python", "JavaScript",
        "React", "Flutter", "Android", "iOS", "Backend Development",
        "UI/UX Design", "DevOps", "Machine Learning"
    )
    
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Select Your Skills:",
            style = MaterialTheme.typography.headlineSmall,
            modifier = Modifier.padding(bottom = 16.dp)
        )
        
        FlowRow(
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            availableSkills.forEach { skill ->
                FilterChip(
                    selected = skill in selectedSkills,
                    onClick = {
                        selectedSkills = if (skill in selectedSkills) {
                            selectedSkills - skill
                        } else {
                            selectedSkills + skill
                        }
                    },
                    label = { Text(skill) },
                    modifier = Modifier.animateItemPlacement()
                )
            }
        }
        
        // Display selected count
        AnimatedVisibility(visible = selectedSkills.isNotEmpty()) {
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = 16.dp)
            ) {
                Text(
                    text = "Selected: ${selectedSkills.size} skills\n${selectedSkills.joinToString(", ")}",
                    modifier = Modifier.padding(16.dp)
                )
            }
        }
    }
}

State Management Best Practices:

  • Use remember { mutableStateOf() } for local component state
  • Implement Set operations for efficient selection/deselection
  • Add AnimatedVisibility for smooth UI transitions

Responsive Design with Flow Layouts

Screen-Size Adaptive Implementation:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ResponsiveImageGrid() {
    val configuration = LocalConfiguration.current
    val screenWidth = configuration.screenWidthDp
    
    val itemsPerRow = when {
        screenWidth < 600 -> 2  // Phone
        screenWidth < 840 -> 3  // Small tablet
        else -> 4               // Large tablet/desktop
    }
    
    val itemSize = (screenWidth - 48) / itemsPerRow // 48 = total padding
    
    FlowRow(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        maxItemsInEachRow = itemsPerRow
    ) {
        repeat(16) { index ->
            AsyncImage(
                model = "https://picsum.photos/${itemSize.toInt()}",
                contentDescription = "Image $index",
                modifier = Modifier
                    .size(itemSize.dp)
                    .clip(RoundedCornerShape(12.dp)),
                contentScale = ContentScale.Crop
            )
        }
    }
}

Responsive Design Principles:

  • Calculate item size dynamically based on screen width
  • Use breakpoints for different device categories
  • Maintain aspect ratios for visual consistency

Performance Optimization Techniques

Memory-Efficient Flow Layouts:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun OptimizedFlowRow(
    items: List<String>,
    onItemClick: (String) -> Unit
) {
    FlowRow(
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        items.forEach { item ->
            key(item) { // Critical for performance with dynamic lists
                OptimizedChip(
                    text = item,
                    onClick = { onItemClick(item) }
                )
            }
        }
    }
}

@Composable
private fun OptimizedChip(
    text: String,
    onClick: () -> Unit
) {
    // Memoize expensive calculations
    val chipColor by remember(text) {
        derivedStateOf {
            generateColorFromString(text)
        }
    }
    
    AssistChip(
        onClick = onClick,
        label = { Text(text) },
        colors = AssistChipDefaults.assistChipColors(
            containerColor = chipColor
        )
    )
}

Performance Tips:

  • Always use key() with dynamic content
  • Memoize expensive calculations with remember
  • Use derivedStateOf for computed values

Comparison: Flow Layouts vs Traditional Layouts

Feature Row/Column LazyRow/LazyColumn FlowRow/FlowColumn
Flexibility Fixed Scrollable Adaptive
Space Management Limited Infinite Content-aware
Performance Excellent Excellent Good
Learning Curve Easy Medium Easy
Best For Fixed items Long lists Dynamic collections
Overflow Handling Manual Scroll Automatic wrap

When to Choose Each:

Use Row/Column when:

  • Element count is fixed and small
  • You need precise control over positioning
  • Simple layouts with predictable content

Use LazyRow/LazyColumn when:

  • Handling large datasets (100+ items)
  • Need infinite scrolling
  • Performance is critical for long lists

Use Flow Layouts when:

  • Content length varies significantly
  • Need automatic responsive behavior
  • Building tag systems or filter interfaces

Advanced Customization: Arrangement and Alignment

Sophisticated Spacing Control:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AdvancedFlowRowArrangement() {
    FlowRow(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceEvenly,
        verticalArrangement = Arrangement.Center,
        maxItemsInEachRow = 3
    ) {
        repeat(9) { index ->
            Card(
                modifier = Modifier
                    .weight(1f)
                    .aspectRatio(1.2f)
            ) {
                Box(
                    contentAlignment = Alignment.Center,
                    modifier = Modifier.fillMaxSize()
                ) {
                    Text(
                        text = "Card ${index + 1}",
                        style = MaterialTheme.typography.titleMedium
                    )
                }
            }
        }
    }
}

Available Arrangement Options:

Horizontal Arrangements:

  • Arrangement.Start - Align to start
  • Arrangement.End - Align to end
  • Arrangement.Center - Center alignment
  • Arrangement.SpaceEvenly - Equal space distribution
  • Arrangement.SpaceBetween - Space between elements
  • Arrangement.SpaceAround - Space around elements

Animation Integration with Flow Layouts

Smooth Transitions for Dynamic Content:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AnimatedTagFlow() {
    var isExpanded by remember { mutableStateOf(false) }
    var selectedTags by remember { mutableStateOf(setOf<String>()) }
    
    val allTags = listOf(
        "Android", "Kotlin", "Compose", "Material Design",
        "Architecture", "Testing", "Performance", "UI/UX",
        "Accessibility", "Internationalization"
    )
    
    val visibleTags = if (isExpanded) allTags else allTags.take(5)
    
    Column {
        FlowRow(
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalArrangement = Arrangement.spacedBy(6.dp),
            modifier = Modifier.animateContentSize(
                animationSpec = spring(
                    dampingRatio = Spring.DampingRatioMediumBouncy,
                    stiffness = Spring.StiffnessLow
                )
            )
        ) {
            visibleTags.forEach { tag ->
                FilterChip(
                    selected = tag in selectedTags,
                    onClick = {
                        selectedTags = if (tag in selectedTags) {
                            selectedTags - tag
                        } else {
                            selectedTags + tag
                        }
                    },
                    label = { Text(tag) },
                    modifier = Modifier.animateItemPlacement()
                )
            }
        }
        
        if (allTags.size > 5) {
            TextButton(
                onClick = { isExpanded = !isExpanded },
                modifier = Modifier.padding(top = 8.dp)
            ) {
                Text(
                    text = if (isExpanded) "Show Less" else "Show More (${allTags.size - 5}+)"
                )
            }
        }
    }
}

Animation Components Explained:

  • animateContentSize(): Smoothly animates layout size changes
  • animateItemPlacement(): Animates individual item position changes
  • Spring animations: Provide natural, bouncy motion
  • dampingRatio: Controls bounce intensity
  • stiffness: Determines animation speed

Common Mistakes and How to Avoid Them

Mistake 1: Forgetting ExperimentalLayoutApi

// Wrong: Will cause compilation error
@Composable
fun BrokenFlowRow() {
    FlowRow { } // Error: Missing @OptIn annotation
}

// Correct: Always include the annotation
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CorrectFlowRow() {
    FlowRow { } // Works perfectly
}

Mistake 2: No Spacing Configuration

// Wrong: Elements stick together
FlowRow {
    items.forEach { item ->
        Chip(text = item)
    }
}

// Correct: Define clear spacing
FlowRow(
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    verticalArrangement = Arrangement.spacedBy(4.dp)
) {
    items.forEach { item ->
        Chip(text = item)
    }
}

Mistake 3: Ignoring Performance with Large Lists

// Wrong: Poor performance with many items
FlowRow {
    (1..1000).forEach { index ->
        Chip(text = "Item $index") // No key, bad performance
    }
}

// Correct: Use key for optimization
FlowRow {
    items.forEach { item ->
        key(item.id) { // Improves recomposition efficiency
            Chip(text = item.name)
        }
    }
}

Real-World Example: E-commerce Product Filter

Complete Implementation:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ProductFilterSystem() {
    var selectedCategories by remember { mutableStateOf(setOf<String>()) }
    var selectedBrands by remember { mutableStateOf(setOf<String>()) }
    var priceRange by remember { mutableStateOf<String?>(null) }
    
    val categories = listOf("Electronics", "Clothing", "Books", "Sports", "Home")
    val brands = listOf("Samsung", "Apple", "Nike", "Adidas", "Sony", "LG")
    val priceRanges = listOf("Under $50", "$50-$200", "$200-$500", "Over $500")
    
    LazyColumn(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(24.dp)
    ) {
        // Categories Filter
        item {
            FilterSection(
                title = "Categories",
                selectedItems = selectedCategories,
                availableItems = categories,
                onSelectionChange = { selectedCategories = it }
            )
        }
        
        // Brands Filter
        item {
            FilterSection(
                title = "Brands",
                selectedItems = selectedBrands,
                availableItems = brands,
                onSelectionChange = { selectedBrands = it }
            )
        }
        
        // Price Range Filter
        item {
            Text(
                text = "Price Range:",
                style = MaterialTheme.typography.titleMedium,
                modifier = Modifier.padding(bottom = 8.dp)
            )
            
            FlowRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp),
                verticalArrangement = Arrangement.spacedBy(4.dp)
            ) {
                priceRanges.forEach { range ->
                    FilterChip(
                        selected = priceRange == range,
                        onClick = {
                            priceRange = if (priceRange == range) null else range
                        },
                        label = { Text(range) }
                    )
                }
            }
        }
        
        // Active Filters Summary
        item {
            ActiveFiltersDisplay(
                selectedCategories = selectedCategories,
                selectedBrands = selectedBrands,
                priceRange = priceRange,
                onClearAll = {
                    selectedCategories = setOf()
                    selectedBrands = setOf()
                    priceRange = null
                }
            )
        }
    }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun FilterSection(
    title: String,
    selectedItems: Set<String>,
    availableItems: List<String>,
    onSelectionChange: (Set<String>) -> Unit
) {
    Column {
        Text(
            text = title,
            style = MaterialTheme.typography.titleMedium,
            modifier = Modifier.padding(bottom = 8.dp)
        )
        
        FlowRow(
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalArrangement = Arrangement.spacedBy(4.dp)
        ) {
            availableItems.forEach { item ->
                FilterChip(
                    selected = item in selectedItems,
                    onClick = {
                        val newSelection = if (item in selectedItems) {
                            selectedItems - item
                        } else {
                            selectedItems + item
                        }
                        onSelectionChange(newSelection)
                    },
                    label = { Text(item) }
                )
            }
        }
    }
}

Accessibility Considerations for Flow Layouts

Making Flow Layouts Accessible:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AccessibleFlowRow() {
    val tags = listOf("Android", "Kotlin", "Compose", "Material")
    var selectedTags by remember { mutableStateOf(setOf<String>()) }
    
    Column {
        Text(
            text = "Select technologies of interest",
            style = MaterialTheme.typography.titleMedium,
            modifier = Modifier.padding(bottom = 8.dp)
        )
        
        FlowRow(
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalArrangement = Arrangement.spacedBy(4.dp),
            modifier = Modifier.semantics {
                contentDescription = "Technology selection filters"
            }
        ) {
            tags.forEach { tag ->
                FilterChip(
                    selected = tag in selectedTags,
                    onClick = {
                        selectedTags = if (tag in selectedTags) {
                            selectedTags - tag
                        } else {
                            selectedTags + tag
                        }
                    },
                    label = { Text(tag) },
                    modifier = Modifier.semantics {
                        contentDescription = if (tag in selectedTags) {
                            "$tag selected, tap to deselect"
                        } else {
                            "$tag not selected, tap to select"
                        }
                    }
                )
            }
        }
    }
}

Accessibility Best Practices:

  • Add semantic descriptions for screen readers
  • Provide clear state feedback (selected/unselected)
  • Use descriptive content descriptions
  • Ensure minimum touch target size (48dp)

Advanced Layout Control with Custom Arrangements

Creating Custom Spacing Logic:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CustomArrangementFlow() {
    val priorities = listOf("High", "Medium", "Low", "Critical", "Optional")
    
    FlowRow(
        horizontalArrangement = object : Arrangement.Horizontal {
            override fun arrange(
                totalSize: Int,
                sizes: IntArray,
                layoutDirection: LayoutDirection,
                density: Density
            ): IntArray {
                // Custom spacing logic
                val spacing = with(density) { 12.dp.roundToPx() }
                val positions = IntArray(sizes.size)
                var currentPosition = 0
                
                sizes.forEachIndexed { index, size ->
                    positions[index] = currentPosition
                    currentPosition += size + spacing
                }
                
                return positions
            }
        },
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        priorities.forEach { priority ->
            PriorityChip(
                text = priority,
                priority = priority
            )
        }
    }
}

@Composable
private fun PriorityChip(text: String, priority: String) {
    val backgroundColor = when (priority) {
        "Critical" -> MaterialTheme.colorScheme.error
        "High" -> MaterialTheme.colorScheme.primary
        "Medium" -> MaterialTheme.colorScheme.secondary
        else -> MaterialTheme.colorScheme.surfaceVariant
    }
    
    AssistChip(
        onClick = { },
        label = { Text(text) },
        colors = AssistChipDefaults.assistChipColors(
            containerColor = backgroundColor
        )
    )
}

Testing Flow Layouts

Unit Testing Strategies:

@Test
fun flowRowDisplaysAllItems() {
    val testTags = listOf("Test1", "Test2", "Test3")
    
    composeTestRule.setContent {
        TestFlowRowComponent(tags = testTags)
    }
    
    // Verify all items are displayed
    testTags.forEach { tag ->
        composeTestRule
            .onNodeWithText(tag)
            .assertIsDisplayed()
    }
}

@Test
fun flowRowHandlesSelection() {
    composeTestRule.setContent {
        InteractiveTagFlow()
    }
    
    // Test selection behavior
    composeTestRule
        .onNodeWithText("Kotlin")
        .performClick()
        .assertIsSelected()
}

Testing Best Practices:

  • Test content visibility across different screen sizes
  • Verify selection states for interactive flows
  • Check accessibility semantics
  • Test performance with large datasets

Migration Guide: From Traditional Layouts to Flow

Step-by-Step Migration:

// Before: Traditional Row with overflow issues
@Composable
fun OldTagImplementation(tags: List<String>) {
    Row {
        tags.forEach { tag ->
            Chip(text = tag) // Overflow problem!
        }
    }
}

// After: Flexible Flow Layout
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun NewTagImplementation(tags: List<String>) {
    FlowRow(
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        tags.forEach { tag ->
            Chip(text = tag) // No overflow, automatic wrapping
        }
    }
}

Migration Checklist:

  • Add ExperimentalLayoutApi annotation
  • Replace Row/Column with FlowRow/FlowColumn
  • Configure spacing arrangements
  • Test on different screen sizes
  • Update any hardcoded positioning logic

Performance Benchmarks and Optimization

Measuring Flow Layout Performance:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun PerformanceOptimizedFlow(
    items: List<TagItem>,
    modifier: Modifier = Modifier
) {
    // Use LaunchedEffect for expensive operations
    val processedItems by remember(items) {
        derivedStateOf {
            items.map { item ->
                ProcessedTagItem(
                    id = item.id,
                    text = item.text,
                    color = generateColor(item.category)
                )
            }
        }
    }
    
    FlowRow(
        modifier = modifier,
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        processedItems.forEach { item ->
            key(item.id) {
                OptimizedTagChip(item = item)
            }
        }
    }
}

@Composable
private fun OptimizedTagChip(item: ProcessedTagItem) {
    // Avoid recreating composables unnecessarily
    AssistChip(
        onClick = { /* Handle click */ },
        label = { Text(item.text) },
        colors = AssistChipDefaults.assistChipColors(
            containerColor = item.color
        )
    )
}

Performance Guidelines:

  • Use key() for all dynamic content
  • Minimize recomposition with remember and derivedStateOf
  • Avoid complex calculations inside the composition
  • Consider LazyLayouts for 50+ items

Advanced Use Case: Multi-Level Category Filter

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MultiLevelCategoryFilter() {
    var selectedMainCategory by remember { mutableStateOf<String?>(null) }
    var selectedSubCategories by remember { mutableStateOf(setOf<String>()) }
    
    val categories = mapOf(
        "Technology" to listOf("Mobile", "Web", "AI", "Cloud", "DevOps"),
        "Design" to listOf("UI/UX", "Graphic", "Product", "Brand"),
        "Business" to listOf("Marketing", "Sales", "Finance", "Strategy")
    )
    
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        // Main Categories
        Text(
            text = "Select Main Category:",
            style = MaterialTheme.typography.titleLarge
        )
        
        FlowRow(
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalArrangement = Arrangement.spacedBy(4.dp)
        ) {
            categories.keys.forEach { category ->
                FilterChip(
                    selected = selectedMainCategory == category,
                    onClick = {
                        selectedMainCategory = if (selectedMainCategory == category) {
                            null
                        } else {
                            category
                        }
                        selectedSubCategories = setOf() // Reset subcategories
                    },
                    label = { Text(category) }
                )
            }
        }
        
        // Sub Categories (shown only when main category is selected)
        selectedMainCategory?.let { mainCategory ->
            AnimatedVisibility(visible = true) {
                Column {
                    Text(
                        text = "Select Subcategories:",
                        style = MaterialTheme.typography.titleMedium,
                        modifier = Modifier.padding(bottom = 8.dp)
                    )
                    
                    FlowRow(
                        horizontalArrangement = Arrangement.spacedBy(6.dp),
                        verticalArrangement = Arrangement.spacedBy(4.dp)
                    ) {
                        categories[mainCategory]?.forEach { subCategory ->
                            FilterChip(
                                selected = subCategory in selectedSubCategories,
                                onClick = {
                                    selectedSubCategories = if (subCategory in selectedSubCategories) {
                                        selectedSubCategories - subCategory
                                    } else {
                                        selectedSubCategories + subCategory
                                    }
                                },
                                label = { Text(subCategory) }
                            )
                        }
                    }
                }
            }
        }
        
        // Results Summary
        if (selectedMainCategory != null || selectedSubCategories.isNotEmpty()) {
            ResultsSummaryCard(
                mainCategory = selectedMainCategory,
                subCategories = selectedSubCategories
            )
        }
    }
}

@Composable
private fun ResultsSummaryCard(
    mainCategory: String?,
    subCategories: Set<String>
) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = "Active Filters:",
                style = MaterialTheme.typography.titleSmall,
                modifier = Modifier.padding(bottom = 8.dp)
            )
            
            mainCategory?.let {
                Text("Main: $it")
            }
            
            if (subCategories.isNotEmpty()) {
                Text("Sub: ${subCategories.joinToString(", ")}")
            }
            
            Text(
                text = "Found: ${calculateResults(mainCategory, subCategories)} items",
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.primary,
                modifier = Modifier.padding(top = 8.dp)
            )
        }
    }
}

private fun calculateResults(mainCategory: String?, subCategories: Set<String>): Int {
    // Simulate result calculation
    return when {
        mainCategory != null && subCategories.isNotEmpty() -> subCategories.size * 15
        mainCategory != null -> 45
        else -> 0
    }
}

Integration with Navigation and State Hoisting

Stateful Flow Layout Component:

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun StatefulTagSelector(
    selectedTags: Set<String>,
    onTagSelectionChange: (Set<String>) -> Unit,
    availableTags: List<String>,
    modifier: Modifier = Modifier
) {
    FlowRow(
        modifier = modifier,
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        availableTags.forEach { tag ->
            FilterChip(
                selected = tag in selectedTags,
                onClick = {
                    val newSelection = if (tag in selectedTags) {
                        selectedTags - tag
                    } else {
                        selectedTags + tag
                    }
                    onTagSelectionChange(newSelection)
                },
                label = { Text(tag) }
            )
        }
    }
}

// Usage in parent composable
@Composable
fun ParentScreen() {
    var userSelectedTags by remember { mutableStateOf(setOf<String>()) }
    
    StatefulTagSelector(
        selectedTags = userSelectedTags,
        onTagSelectionChange = { userSelectedTags = it },
        availableTags = listOf("Android", "iOS", "Web", "Backend")
    )
}

Future-Proofing Your Flow Layouts

Preparing for Stable API:

// Current implementation with experimental API
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CurrentFlowImplementation() {
    FlowRow {
        // Implementation
    }
}

// Recommended wrapper for easier migration
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AppFlowRow(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    maxItemsInEachRow: Int = Int.MAX_VALUE,
    content: @Composable () -> Unit
) {
    FlowRow(
        modifier = modifier,
        horizontalArrangement = horizontalArrangement,
        verticalArrangement = verticalArrangement,
        maxItemsInEachRow = maxItemsInEachRow,
        content = content
    )
}

Migration Preparation:

  • Create wrapper functions for easy API updates
  • Document experimental dependencies clearly
  • Plan for stable API migration in future releases

Conclusion: Mastering Flow Layouts in Jetpack Compose

Flow Layouts in Jetpack Compose revolutionize how we handle dynamic UI content in Android applications. From simple tag displays to complex multi-level filters, FlowRow and FlowColumn provide the flexibility modern apps demand.

Key Takeaways:

  • Flow Layouts automatically adapt to content and screen size
  • Perfect for tags, filters, and dynamic content collections
  • Require ExperimentalLayoutApi annotation
  • Offer superior user experience compared to traditional layouts
  • Integrate seamlessly with Compose's animation and state systems

Next Steps:

Start implementing Flow Layouts in your current projects. Begin with simple tag displays, then gradually incorporate advanced features like state management and animations. Your users will appreciate the responsive, polished interface that adapts to their content needs.

Ready to transform your Android UI? Begin experimenting with Flow Layouts in Jetpack Compose today and discover the power of truly adaptive layouts.