Effortlessly Build Collapsible Menus in Jetpack Compose

Jetpack Compose, Google’s modern toolkit for building native UI in Android, has revolutionized how developers design and implement user interfaces. Among its many strengths is its declarative approach, which makes creating dynamic UI components like collapsible menus intuitive and efficient. In this blog post, we’ll dive deep into building collapsible menus using Jetpack Compose, exploring best practices, advanced use cases, and optimization techniques.

What Are Collapsible Menus?

Collapsible menus, also known as expandable or accordion menus, are UI components that allow users to reveal or hide nested content dynamically. These menus enhance user experience by organizing content hierarchically and improving navigation clarity in complex applications.

Common use cases for collapsible menus include:

  • Navigation drawers with nested items

  • Filter panels in shopping apps

  • FAQ sections in help centers

With Jetpack Compose, building these menus becomes straightforward, leveraging state management and composable functions.

Core Concepts in Jetpack Compose for Collapsible Menus

Before jumping into the implementation, it’s essential to understand the key Jetpack Compose concepts used in collapsible menus:

1. State Management

State in Jetpack Compose controls the visibility of menu items. The MutableState class from Compose’s State API enables real-time UI updates based on user interactions.

2. Animation APIs

Compose’s animate* functions, such as animateDpAsState and animateFloatAsState, make transitions smooth and visually appealing when toggling menu visibility.

3. Composition Local

For menus nested within larger layouts, CompositionLocal provides a seamless way to share state and properties across composables.

Building a Basic Collapsible Menu

Let’s start with a simple implementation of a collapsible menu. This example includes a parent menu item that expands to reveal a list of child items.

Code Implementation

@Composable
fun CollapsibleMenu(title: String, items: List<String>) {
    var isExpanded by remember { mutableStateOf(false) }

    Column {
        // Menu Header
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .clickable { isExpanded = !isExpanded }
                .padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                text = title,
                style = MaterialTheme.typography.h6,
                modifier = Modifier.weight(1f)
            )
            Icon(
                imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
                contentDescription = null
            )
        }

        // Expandable Content
        if (isExpanded) {
            Column(modifier = Modifier.padding(start = 16.dp)) {
                items.forEach { item ->
                    Text(
                        text = item,
                        style = MaterialTheme.typography.body1,
                        modifier = Modifier.padding(vertical = 8.dp)
                    )
                }
            }
        }
    }
}

Key Highlights:

  • The mutableStateOf function tracks whether the menu is expanded.

  • The Row composable creates a clickable header for toggling menu visibility.

  • A simple if condition renders the child items dynamically.

Enhancing the Menu with Animations

To improve user experience, let’s add animations that smoothly expand or collapse the menu.

Animated Version

@Composable
fun AnimatedCollapsibleMenu(title: String, items: List<String>) {
    var isExpanded by remember { mutableStateOf(false) }
    val transitionState = updateTransition(targetState = isExpanded, label = "MenuTransition")

    val contentHeight by transitionState.animateDp(label = "ContentHeight") { expanded ->
        if (expanded) 200.dp else 0.dp
    }

    Column {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .clickable { isExpanded = !isExpanded }
                .padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                text = title,
                style = MaterialTheme.typography.h6,
                modifier = Modifier.weight(1f)
            )
            Icon(
                imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
                contentDescription = null
            )
        }

        Box(
            modifier = Modifier
                .height(contentHeight)
                .clipToBounds()
                .padding(start = 16.dp)
        ) {
            Column {
                items.forEach { item ->
                    Text(
                        text = item,
                        style = MaterialTheme.typography.body1,
                        modifier = Modifier.padding(vertical = 8.dp)
                    )
                }
            }
        }
    }
}

Enhancements:

  • The updateTransition API animates state changes.

  • The animateDp function smoothly transitions the menu’s height.

  • The clipToBounds modifier ensures content remains visually contained within bounds.

Best Practices for Collapsible Menus

When building collapsible menus, follow these best practices to ensure scalability and performance:

1. Lazy Rendering

For menus with a large number of items, use LazyColumn instead of Column to optimize memory usage.

LazyColumn {
    items(items) { item ->
        Text(
            text = item,
            style = MaterialTheme.typography.body1,
            modifier = Modifier.padding(vertical = 8.dp)
        )
    }
}

2. Accessibility

Ensure the menu is accessible by providing meaningful contentDescription values for icons and maintaining focus states.

3. State Hoisting

Hoist the state outside of the composable for better reusability and testability:

@Composable
fun CollapsibleMenu(title: String, items: List<String>, isExpanded: Boolean, onToggle: () -> Unit) {
    // Implementation
}

4. Theming and Customization

Leverage MaterialTheme and modifiers to align menus with your app’s design system.

Advanced Use Cases

1. Nested Collapsible Menus

Create menus with nested levels by composing multiple instances of CollapsibleMenu:

@Composable
fun NestedCollapsibleMenu(title: String, subMenus: Map<String, List<String>>) {
    Column {
        subMenus.forEach { (key, items) ->
            CollapsibleMenu(title = key, items = items)
        }
    }
}

2. Dynamic Data Loading

Integrate collapsible menus with APIs or databases to dynamically load content.

val menuItems by viewModel.menuItems.collectAsState()
CollapsibleMenu(title = "Dynamic Menu", items = menuItems)

Conclusion

Jetpack Compose simplifies the creation of collapsible menus, allowing developers to focus on functionality and user experience. By combining state management, animations, and best practices, you can build intuitive and scalable UI components for modern Android apps. Whether you’re creating navigation drawers or nested FAQs, Jetpack Compose provides the tools to do it effortlessly.

Explore the power of Jetpack Compose and take your UI development to the next level!