'What is the better or easier way to create "nested" menus in Jetpack Compose?

So in XML you were able to structure menu items and nest them like this.

xml menu structure


But in jetpack compose, I am unable to figure out how this would work. I already read and built a simple drop down menu from here. But trying to do the same as XML in jetpack compose doesn't make much sense. The menus are created separately and independent. I am looking for something simpler and better than that.

jetpack-compose menu



Solution 1:[1]

How about something like this?

Create a composable for your main menu, and one for your nested menu.

Main Menu Composable

@Composable
fun MainMenu(
    menuSelection: MutableState<MenuSelection>,
    expandedMain: MutableState<Boolean>,
    expandedNested: MutableState<Boolean>
) {
    DropdownMenu(
        expanded = expandedMain.value,
        onDismissRequest = { expandedMain.value = false },
    ) {
        DropdownMenuItem(
            onClick = {
                expandedMain.value = false // hide main menu
                expandedNested.value = true // show nested menu
                menuSelection.value = MenuSelection.NESTED
            }
        ) {
            Text("Nested Options \u25B6")
        }

        Divider()

        DropdownMenuItem(
            onClick = {
                // close main menu
                expandedMain.value = false
                menuSelection.value = MenuSelection.SETTINGS
            }
        ) {
            Text("Settings")
        }

        Divider()

        DropdownMenuItem(
            onClick = {
                // close main menu
                expandedMain.value = false
                menuSelection.value = MenuSelection.ABOUT
            }
        ) {
            Text("About")
        }
    }
}

Nested Menu Composable

@Composable
fun NestedMenu(
    expandedNested: MutableState<Boolean>,
    nestedMenuSelection: MutableState<NestedMenuSelection>
) {
    DropdownMenu(
        expanded = expandedNested.value,
        onDismissRequest = { expandedNested.value = false }
    ) {
        DropdownMenuItem(
            onClick = {
                // close nested menu
                expandedNested.value = false
                nestedMenuSelection.value = NestedMenuSelection.FIRST
            }
        ) {
            Text("First")
        }
        DropdownMenuItem(
            onClick = {
                // close nested menu
                expandedNested.value = false
                nestedMenuSelection.value = NestedMenuSelection.SECOND
            }
        ) {
            Text("Second")
        }
    }
}

Then place them in your main drop down menu composable.

Top Bar Composable

@Composable
fun TopAppBarDropdownMenu(
    menuSelection: MutableState<MenuSelection>,
    nestedMenuSelection: MutableState<NestedMenuSelection>
) {

    val expandedMain = remember { mutableStateOf(false) }
    val expandedNested = remember { mutableStateOf(false) }

    // Three Dot icon
    Box(
        Modifier
            .wrapContentSize(Alignment.TopEnd)
    ) {
        IconButton(
            onClick = {
                // Expand the main menu on three dots icon click
                // and hide the nested menu. 
                expandedMain.value = true
                expandedNested.value = false
            }
        ) {
            Icon(
                Icons.Filled.MoreVert,
                contentDescription = "More Menu"
            )
        }
    }

    MainMenu(
        menuSelection = menuSelection,
        expandedMain = expandedMain,
        expandedNested = expandedNested
    )
    NestedMenu(
        expandedNested = expandedNested,
        nestedMenuSelection = nestedMenuSelection
    )

}

The menuSelection and nestedMenuSelection parameters of TopAppBarDropdownMenu would allow implementing actions in the TopAppBarDropdownMenu host depending on which state is activated from the menu. Something like this:

@Composable
fun MainScreen(
    /* some params */
) {
    
    val menuSelection = remember { mutableStateOf(MenuSelection.NONE) }
    val nestedMenuSelection = remember { mutableStateOf(NestedMenuSelection.DEFAULT) }
    
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(text = stringResource(R.string.app_name)) },
                actions = {
                    TopAppBarDropdownMenu(
                        menuSelection = menuSelection,
                        nestedMenuSelection= nestedMenuSelection
                    )
                }
            )
        }
    )
}

Not sure it's the "right" way to do it but it works for me.

You could potentially make it easier to instantiate nested menus by creating a NestedMenu composable with more parameters, which would make it easier to reuse. Just keep in mind that Material Design guidelines recommend using Cascading Menus only on desktop.

Solution 2:[2]

I am wondering this question as well. At present, I compose this nested Menu like this:

@Composable
fun ExpandableDropdownItem(
    text: String,
    dropDownItems: @Composable () -> Unit
) {
    var expanded by remember {
        mutableStateOf(false)
    }
    DropdownMenuItem(onClick = {
        expanded = true
    }) {
        Text(text = text)
    }
    DropdownMenu(
        expanded = expanded,
        onDismissRequest = { expanded = false }
    ) {
        dropDownItems()
    }
}

This can be used with other DropdownItems. And that's not the best solution : the place of the nested Menu is strange.

Solution 3:[3]

I made a nested menu variation for sorting that seems to work well using AnimatedVisibility.

When expanding, it switches the top level menus visibility to the submenus, and can even add a back button if needed, but more levels might get confusing with the switches.

@Composable
fun NestedSortMenu(
    onSortClick: (SortOrder) -> Unit
) {
    var expanded by remember { mutableStateOf(false) }
    var orderType: OrderType by remember { mutableStateOf(OrderType.Ascending) }

    AnimatedVisibility(visible = !expanded) {
        Column {
            DropdownMenuItem(onClick = {
                orderType = OrderType.Ascending
                expanded = !expanded
            }) {
                Text(text = stringResource(R.string.ascending))
                Icon(
                    Icons.Filled.NavigateNext,
                    contentDescription = stringResource(R.string.ascending),
                    modifier = Modifier.size(20.dp)
                )
            }
            DropdownMenuItem(onClick = {
                orderType = OrderType.Descending
                expanded = !expanded
            }) {
                Text(text = stringResource(R.string.descending))
                Icon(
                    Icons.Filled.NavigateNext,
                    contentDescription = stringResource(R.string.descending),
                    modifier = Modifier.size(20.dp)
                )
            }
        }

    }
    AnimatedVisibility(visible = expanded) {
        Column {
            DropdownMenuItem(onClick = {
                expanded = !expanded
                onSortClick(SortOrder.Title(orderType))
            }) {
                Text(text = stringResource(R.string.title_menu))
            }
            DropdownMenuItem(onClick = {
                expanded = !expanded
                onSortClick(SortOrder.Format(orderType))
            }) {
                Text(text = stringResource(R.string.format_menu))
            }
            DropdownMenuItem(onClick = {
                expanded = !expanded
                onSortClick(SortOrder.DateAndTime(orderType))
            }) {
                Text(text = stringResource(R.string.date_and_time))
            }
        }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 FunnySaltyFish
Solution 3 Patrik