How to Implement a Dropdown Menu in Jetpack Compose?

jetpack compose dropdown menu

In this article, we will learn dropdown menu APIs in Jetpack Compose with examples.

Prerequisites:

What is a Dropdown Menu in Android?

A dropdown menu displays multiple choices. Users can tap on any one of them. We display the menu when the user interacts with a UI element (like an icon or button) or performs a specific action.

Jetpack Compose provides 2 dropdown menu APIs:

  1. Dropdown Menu
  2. Exposed Dropdown Menu

Dropdown Menu:

This is a normal menu.

Example:

Jetpack compose dropdown menu

The drop down menu uses the position of the parent layout to position itself on the screen. Usually, we place the drop down menu in a box with a sibling (like a button) that will be used as the ‘anchor’. Whenever we tap on this anchor, the menu will be displayed.

Let’s make the above menu.

First, create a MyUI() composable outside the MainActivity class. We will write our code in it.

@Composable
fun MyUI() {

}

Call it inside the onCreate() method like this:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            YourProjectNameTheme(darkTheme = false) {
                Column(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(color = Color.White),
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    MyUI()
                }
            }
        }
    }
}

@Composable
fun MyUI() {

}

Create the items list using the Kotlin array function.

val listItems = arrayOf("Favorites", "Options", "Settings", "Share")

The second item is disabled in the above menu. So, create a variable and store its index value. Also, we will display the Toast when the user taps on the menu item. We need a context for that.

@Composable
fun MyUI() {
    val listItems = arrayOf("Favorites", "Options", "Settings", "Share")
    val disabledItem = 1
    val contextForToast = LocalContext.current.applicationContext
}

The menu should be displayed when we tap on the icon. Create an IconButton() and put it inside a Box.

Box(
    contentAlignment = Alignment.Center
) {

    IconButton(onClick = {
        // we will open the menu here
    }) {
        Icon(
            imageVector = Icons.Default.MoreVert,
            contentDescription = "Open Options"
        )
    }
}

Let’s look at the DropdownMenu() API.

@Composable
fun DropdownMenu(
    expanded: Boolean,
    onDismissRequest: () -> Unit,
    modifier: Modifier = Modifier,
    offset: DpOffset = DpOffset(0.dp, 0.dp),
    properties: PopupProperties = PopupProperties(focusable = true),
    content: @Composable ColumnScope.() -> Unit
)

expanded – Whether the menu is currently open and visible to the user.

onDismissRequest – Called when the user requests to dismiss the menu, such as by tapping on the screen outside the menu’s bounds or when the back key is pressed.

modifierThe modifier.

offset – It is used to control the position of the menu on the screen.

properties – Used to customize the behavior of the menu.

content – The content of the menu. It typically includes DropdownMenuItems as well as custom content. Note that the content is placed inside a scrollable Column. So, If you use a LazyColumn as the root layout, you will end up getting an error.

Create a variable to handle the state of the menu.

// state of the menu
var expanded by remember {
    mutableStateOf(false)
}

Box(
    contentAlignment = Alignment.Center
) {

    // options button
    IconButton(onClick = {
        expanded = true
    }) {
        Icon(
            imageVector = Icons.Default.MoreVert,
            contentDescription = "Open Options"
        )
    }

    // drop down menu
    DropdownMenu(
        expanded = expanded,
        onDismissRequest = {
            expanded = false
        }
    ) {

    }
}

We need to add the menu items using the DropdownMenuItem() composable. Here is how it looks:

@Composable
fun DropdownMenuItem(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    content: @Composable RowScope.() -> Unit
)

onClick – Called when the item was clicked.

enabled – If the item is enabled. If it is false, the item will not be clickable, and onClick will not be invoked.

contentPaddingThe padding applied to the content of this menu item.

interactionSource – The MutableInteractionSource.

content – The content of the menu item. It is a row scope. So, if you add multiple composables, they are placed horizontally.

Since our listItems is of type Array, we can use Kotlin’s forEachIndexed() to place them inside the menu.

listItems.forEachIndexed { itemIndex, itemValue ->
    DropdownMenuItem(
        onClick = {
            Toast.makeText(contextForToast, itemValue, Toast.LENGTH_SHORT)
                .show()
            expanded = false
        },
        enabled = (itemIndex != disabledItem)
    ) {
        Text(text = itemValue)
    }
}

Our final code looks like this:

@Composable
fun MyUI() {
    val listItems = arrayOf("Favorites", "Options", "Settings", "Share")
    val disabledItem = 1
    val contextForToast = LocalContext.current.applicationContext

    // state of the menu
    var expanded by remember {
        mutableStateOf(false)
    }

    Box(
        contentAlignment = Alignment.Center
    ) {

        // options button
        IconButton(onClick = {
            expanded = true
        }) {
            Icon(
                imageVector = Icons.Default.MoreVert,
                contentDescription = "Open Options"
            )
        }

        // drop down menu
        DropdownMenu(
            expanded = expanded,
            onDismissRequest = {
                expanded = false
            }
        ) {
            // adding items
            listItems.forEachIndexed { itemIndex, itemValue ->
                DropdownMenuItem(
                    onClick = {
                        Toast.makeText(contextForToast, itemValue, Toast.LENGTH_SHORT)
                            .show()
                        expanded = false
                    },
                    enabled = (itemIndex != disabledItem)
                ) {
                    Text(text = itemValue)
                }
            }
        }
    }
}

Run it, you will see the following output:

Dropdown Menu Position

In the above output, the menu is placed on the left side. If you don’t like it, we can change it using the offset parameter of the DropdownMenu(). It accepts the value of type DpOffset which can be obtained from the DpOffset() function.

DropdownMenu(
    // previous parameters here
    offset = DpOffset(x = (-66).dp, y = (-10).dp)
)

Play around with x and y values until you are happy.

Output:

Jetpack compose dropdown menu

You can find more about offset in this Canvas article.

Exposed Dropdown Menu:

It displays the currently selected item above the list. It is divided into two types – read-only and editable.

Read-only Exposed Dropdown Menu:

It just displays the menu. We cannot search for the menu items.

Example:

Read-only Menu

Let’s implement it. If you look at the output, there are 2 elements – TextField and menu. They are placed inside a box.

Jetpack Compose provides two APIs:

ExposedDropdownMenu – It is the menu that shows the items.
ExposedDropdownMenuBox – It is the box that contains the TextField and the menu.

This is how they look:

Menu:

@Composable
fun ExposedDropdownMenu(
    expanded: Boolean,
    onDismissRequest: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit
)

expanded – Whether the menu is currently open and visible to the user.

onDismissRequest – Called when the user requests to dismiss the menu, such as by tapping on the screen outside the menu’s bounds or pressing the back button.

content – The content of the dropdown menu, typically a DropdownMenuItem.

Menu Box:

@ExperimentalMaterialApi
@Composable
fun ExposedDropdownMenuBox(
    expanded: Boolean,
    onExpandedChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable ExposedDropdownMenuBoxScope.() -> Unit
)

expanded – Whether the Dropdown Menu should be expanded or not.

onExpandedChange – It is called when the user clicks on this menu box.

content – The content to be displayed inside ExposedDropdownMenuBox. It typically contains TextField and ExposedDropdownMenu.

Replace the MyUI() with the following code:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyUI() {

    val contextForToast = LocalContext.current.applicationContext

    val listItems = arrayOf("Favorites", "Options", "Settings", "Share")

    var selectedItem by remember {
        mutableStateOf(listItems[0])
    }

    var expanded by remember {
        mutableStateOf(false)
    }
}

We have added an extra selectedItem variable. When we tap on the menu item, it remembers the value and displays it in the TextField. Let’s create the menu box.

ExposedDropdownMenuBox(
    expanded = expanded,
    onExpandedChange = {
        expanded = !expanded
    }
) {

}

We should put the TextField and menu in it.

TextField:

TextField(
    value = selectedItem,
    onValueChange = {},
    readOnly = true,
    label = { Text(text = "Label") },
    trailingIcon = {
        ExposedDropdownMenuDefaults.TrailingIcon(
            expanded = expanded
        )
    },
    colors = ExposedDropdownMenuDefaults.textFieldColors()
)

Menu:

ExposedDropdownMenu(
    expanded = expanded,
    onDismissRequest = { expanded = false }
) {
    listItems.forEach { selectedOption ->
        // menu item
        DropdownMenuItem(onClick = {
            selectedItem = selectedOption
            Toast.makeText(contextForToast, selectedOption, Toast.LENGTH_SHORT).show()
            expanded = false
        }) {
            Text(text = selectedOption)
        }
    }
}

Here is the final code:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyUI() {

    val contextForToast = LocalContext.current.applicationContext

    val listItems = arrayOf("Favorites", "Options", "Settings", "Share")

    var selectedItem by remember {
        mutableStateOf(listItems[0])
    }

    var expanded by remember {
        mutableStateOf(false)
    }

    // the box
    ExposedDropdownMenuBox(
        expanded = expanded,
        onExpandedChange = {
            expanded = !expanded
        }
    ) {

        // text field
        TextField(
            value = selectedItem,
            onValueChange = {},
            readOnly = true,
            label = { Text(text = "Label") },
            trailingIcon = {
                ExposedDropdownMenuDefaults.TrailingIcon(
                    expanded = expanded
                )
            },
            colors = ExposedDropdownMenuDefaults.textFieldColors()
        )

        // menu
        ExposedDropdownMenu(
            expanded = expanded,
            onDismissRequest = { expanded = false }
        ) {
            listItems.forEach { selectedOption ->
                // menu item
                DropdownMenuItem(onClick = {
                    selectedItem = selectedOption
                    Toast.makeText(contextForToast, selectedOption, Toast.LENGTH_SHORT).show()
                    expanded = false
                }) {
                    Text(text = selectedOption)
                }
            }
        }
    }
}

Output:

Read-only Menu

Editable Exposed Dropdown Menu:

It accepts the input via TextField. It filters out the menu items based on the input.

Example:

Editable Menu

The code is similar to the read-only dropdown menu. But, there are two differences. The TextField() is editable and we filter the menu items as the user enters the data.

Here is the code:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyUI() {

    val listItems = arrayOf("Favorites", "Options", "Settings", "Share")

    var selectedItem by remember {
        mutableStateOf("")
    }

    var expanded by remember {
        mutableStateOf(false)
    }

    ExposedDropdownMenuBox(
        expanded = expanded,
        onExpandedChange = {
            expanded = !expanded
        }
    ) {

        TextField(
            value = selectedItem,
            onValueChange = { selectedItem = it },
            label = { Text(text = "Label") },
            trailingIcon = {
                ExposedDropdownMenuDefaults.TrailingIcon(
                    expanded = expanded
                )
            },
            colors = ExposedDropdownMenuDefaults.textFieldColors()
        )

        // filter options based on text field value
        val filteringOptions =
            listItems.filter { it.contains(selectedItem, ignoreCase = true) }

        if (filteringOptions.isNotEmpty()) {

            ExposedDropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false }
            ) {
                filteringOptions.forEach { selectionOption ->
                    DropdownMenuItem(
                        onClick = {
                            selectedItem = selectionOption
                            expanded = false
                        }
                    ) {
                        Text(text = selectionOption)
                    }
                }
            }
        }
    }
}

Instead of TextFiled, you can also use OutlinedTextField.

This is all about dropdown menu APIs in Jetpack Compose. I hope you have learned something new. If you have any doubts, comment below.

Continue Exploring Jetpack Compose:

References:

Leave a Comment