How to Implement a Dropdown Menu in Jetpack Compose?

jetpack compose dropdown menu

In this article, we will learn how to implement dropdown menu APIs in Jetpack Compose.

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

This is a normal menu.

Example:

Jetpack compose dropdown menu

There are two elements – the icon button (3 vertical dots) and the menu. We place them in a box layout so that the menu is displayed on the screen properly.

Here is the complete code:

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext

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

@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
    ) {
        // 3 vertical dots icon
        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 and tap on the icon button. You will see the menu.

Jetpack compose dropdown menu

Let’s understand the code.

First, we created 4 variables:

listItems – It is the list of items displayed on the menu.

disabledItem – The second item (Options) is disabled on the menu. We are storing its index value (1) here.

contextForToast – It is the context object for the toast message. We display toast when we tap on the item. In Jetpack Compose, we can get the context from LocalContext.current object.

expanded – It is the state of the menu. If it is true, the menu will be displayed. Otherwise, the menu will be removed from the screen.

Next, we have added a Box. We have put the IconButton and the DropdownMenu in it. The menu API looks like this:

@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. We passed our expanded object to it.

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. We set the expanded false so that the menu will be dismissed.

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 – This is the content of the menu. We have added the items using the DropdownMenuItem() composable. The content block is a scrollable Column. So, if you use LazyColumn as the root layout, you will end up getting an error.

DropdownMenuItem() looks like this:

@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 – Whether 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 are using the Kotlin’s forEachIndexed() to place them inside the menu.

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.

2. 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

It contains 2 elements – TextField and menu. If we tap on the TextField, it shows the menu.

Here is the complete code:

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext

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

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyUI() {
    val listItems = arrayOf("Favorites", "Options", "Settings", "Share")
    val contextForToast = LocalContext.current.applicationContext

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

    // remember the selected item
    var selectedItem by remember {
        mutableStateOf(listItems[0])
    }

    // 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 }
        ) {
            // this is a column scope
            // all the items are added vertically
            listItems.forEach { selectedOption ->
                // menu item
                DropdownMenuItem(onClick = {
                    selectedItem = selectedOption
                    Toast.makeText(contextForToast, selectedOption, Toast.LENGTH_SHORT).show()
                    expanded = false
                }) {
                    Text(text = selectedOption)
                }
            }
        }
    }
}

Run it and you will see the following output:

Read-only Menu

The code is similar to the normal dropdown menu, but there are a few changes.

First, we created selectedItem variable. It is used to display the selected item in the TextField.

Next, instead of Box(), we are using ExposedDropdownMenuBox(). It takes 4 parameters:

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

expanded – Whether the Dropdown Menu is expanded or not.

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

content – The content to be displayed inside the box. We have added TextField and ExposedDropdownMenu. Let’s look at the menu API:

@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. We are adding the items using the DropdownMenuItem() composable.

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:

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext

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

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyUI() {
    val contextForToast = LocalContext.current.applicationContext
    val listItems = arrayOf("Favorites", "Options", "Settings", "Share")

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

    // remember the selected item
    var selectedItem by remember {
        mutableStateOf(listItems[0])
    }

    // box
    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()) {
            // menu
            ExposedDropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false }
            ) {
                // this is a column scope
                // all the items are added vertically
                filteringOptions.forEach { selectionOption ->
                    // menu item
                    DropdownMenuItem(
                        onClick = {
                            selectedItem = selectionOption
                            Toast.makeText(contextForToast, selectedItem, Toast.LENGTH_SHORT).show()
                            expanded = false
                        }
                    ) {
                        Text(text = selectionOption)
                    }
                }
            }
        }
    }
}

Run it and you will see the following output:

Editable Menu

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.

Related Articles:

References:

12 thoughts on “How to Implement a Dropdown Menu in Jetpack Compose?”

  1. It did not work when I tried running the code. Something wrong with line 77 as this invoked three error messages:
    1: “No passed value for parameter ‘text’ ” (line 77)
    2: “Type mismatch: inferred type is () -> Unit but mutableInteractionSource was expected” (line 77)
    3: “@Composable invocations can only happen from the context of a @Composable function.

    Hope you can help me figure out what is wrong 🙂

    Reply
  2. 1) “Instead of TextFiled, you can also use OutlinedTextField.”, may be “Instead of TextField…

    2) I try.. The examples not work.

    If possible, I would like to see the solution. Thanks.

    Reply
  3. I think it’s totally unhinged that there isn’t a basic built-in Dropdown List/Menu in Compose. Sure, just like for a TextField, you could create your own fancy one with a bunch of options and functionality and appearance, but I think it’s totally ridiculous you cannot create a BASIC UI element in a few lines of code. Nuttier than squirrel feces

    Reply

Leave a Comment