Adding a dropdown menu to a Jetpack Compose TopAppBar

Adding a dropdown menu to a Jetpack ComposeTopAppBar is quite easy, but simple examples aren't so easy to find! This post has a canonical example to use as a basis for your dropdown menu.

Add a Scaffold that Includes a TopAppBar

The first step is to create the Compose view as a Scaffold that assigns a new TopAppBar to the topBar property. This snippet will create a minimal top bar we can use as a starter.

val bodyContent = remember { mutableStateOf("Body Content Here") }

Scaffold(
  topBar = {
    TopAppBar(
      title = {
        Text(text = "App Title")
      },
      actions = {
          // TODO: Implement the menu
      })
    }) {
    Column(
        horizontalAlignment = 
              Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxWidth()
                          .padding(20.dp)) {
          Text(bodyContent.value)
  }
}

Implement the menu

On line eight of the above snippit is a TODO item to implement the menu. This could be implemented in-line by adding the menu as a replacement for the TODO comment line, but that would result in a really long Composable function that's difficult to read. Instead let's break that out into its own Composable function. Below the code we'll discuss how it works.

@Composable
fun TopAppBarDropdownMenu(bodyContent: MutableState<String>) {
    val expanded = remember { mutableStateOf(false) } // 1

    Box(
        Modifier
            .wrapContentSize(Alignment.TopEnd)
    ) {
        IconButton(onClick = {
            expanded.value = true // 2
            bodyContent.value =  "Menu Opening"
        }) {
            Icon(
                Icons.Filled.MoreVert,
                contentDescription = "More Menu"
            )
        }
    }

    DropdownMenu(
        expanded = expanded.value,
        onDismissRequest = { expanded.value = false },
    ) {
        DropdownMenuItem(onClick = {
            expanded.value = false // 3
            bodyContent.value = "First Item Selected"  // 4
        }) {
            Text("First item")
        }

        Divider()

        DropdownMenuItem(onClick = {
            expanded.value = false
            bodyContent.value = "Second Item Selected"
        }) {
            Text("Second item")
        }

        Divider()

        DropdownMenuItem(onClick = {
            expanded.value = false
            bodyContent.value = "Third Item Selected"
        }) {
            Text("Third item")
        }

        Divider()

        DropdownMenuItem(onClick = {
            expanded.value = false
            bodyContent.value = "Fourth Item Selected"
        }) {
            Text("Fourth item")
        }
    }
}

The structure of the menu uses the standard Compose DropDownMenu composable embedded in a Box. There are a few interesting points to discuss, called out by comments in the above code.

  1. At //1 we add a state variable remember to inform the Composable menu whether it's open or close. The remember {} syntax is used to ensure the state variable survives the view being recomposed.
  2. At //2 the expanded state variable is set to true when the user taps on vertical ellipses icon at the top right of the screen. This causes the DropDownMenu to be redrawn in the open state.
  3. At //3 the expanded state is set to false when the user taps on a menu item, which causes the menu Composable to close. This action is repeated for each element in the menu.
  4. As the menu closes, the code at //4 sets the text value for the bodyContent that was passed in as a parameter to the menu composable.

Point 4 may be a little confusing, so let's talk about it.

Take another look at the first code snippet and notice that this state variable was defined:

val bodyContent = remember { mutableStateOf("Body Content Here") }

Then the current value of this state variable was used as the body content for the Composable view body of the Scaffold:

Column { // some details removed for clarity
  Text(bodyContent.value)
}

And finally, the state variable was passed to the menu composable.

TopAppBarDropdownMenu(bodyContent)

Since bodyContent is a MutableState<String> rather than String, it's a bit like passing a variable by reference--the called routine can update the .value property of the state variable directly.

Get the Code

And here's where you can find the full code for this simple example app to see all the code snippets in the context of a simple (but working) example.

Jetpack Compose Dropdown Menu Example on Github