Android/Kotlin/Jetpack Compose: Dropdown Selectable List/Menu

Itsuki
7 min readFeb 10, 2024

--

In this article, let’s how we can create a dropdown selectable list like following in Android using Kotlin/Jetpack Compose.

Specifically, here is what we are aiming at.

  • A button triggered dropdown list that will be displayed on top of other contents
  • Dropdown becomes scrollable if greater than a max height
  • Dismiss the dropdown on item select and tap outside
  • Button title based on what we have select
  • pass the selected index back to the parent compose to show some other stuff based on the index

Getting Start

Let’s get started by creating a new project.

To create a new project, choose File > New > New Project.

Choose Empty Activity.

Choose Next.

Give your Project a Name and somewhere to save. For Build configuration language, choose Kotlin DSL. Choose Finish to create the project.

Navigate to build.gradle.kts and make sure you have the following dependencies implementations added.

implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")

Open MainActivity.kt to add the following imports (you might already have some of them added for you by default). We will be using those later on.


import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Divider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties

Dropdown List Composable

Let’s first create an empty composable to observe some of the parameters we would want to pass in to it to construct our dropdown list.

@Composable
fun DropdownList(
itemList: List<String>,
selectedIndex: Int,
modifier: Modifier,
onItemClick: (Int) -> Unit) {}

Note that I am passing selectedIndex and onItemClick action to the composable as parameters instead of declaring the selectedIndex as a state variable within the Composable. This is because we would normally want to display some other contents in our parent compassable based on what we have selected, and therefore we would want our state variable selectedIndex to be in our parent Composable instead.

The onItemClick lambda will take in an Integer as its parameter and return nothing. This is the function we will be using when an item in the dropdown list is selected to update the selectedIndex state variable in the parent view. The integer parameter will simply be the index of the item.

The modifier parameter will allow us to configure some of the basic button appearances such as width and height.

Now that we have obtained a basic image for our DropdownList Composable, let’s move onto creating the actual components.

Button

This will be the control to show the list that will be initially hidden from our user.

To keep track of whether if the list is displayed or not, we will first need a showDropdown state variable like following.

var showDropdown by rememberSaveable { mutableStateOf(false) }

Our Button will be as simple as a Box with some Text.

// button
Box(
modifier = modifier
.background(Color.Red)
.clickable { showDropdown = true },
// .clickable { showDropdown = !showDropdown },
contentAlignment = Alignment.Center
) {
Text(text = itemList[selectedIndex], modifier = Modifier.padding(3.dp))
}

I have assigned the clickable action to showDropdown = true to always open the list regardless of its current state. If you want the button to be able to toggle the list instead, you can use showDropdown = !showDropdown.

Dropdown List

Now that we have our button created , let’s move onto creating the list part.

First of all, Since our item list might be really long and our list might become scrollable, we will need to keep track of the scroll position by using the following state variable. Make sure to add it to the top of the Composable function.

val scrollState = rememberScrollState()

Below is the code for the Dropdown List.

Box() {
if (showDropdown) {
Popup(
alignment = Alignment.TopCenter,
properties = PopupProperties(
excludeFromSystemGesture = true,
),
// to dismiss on click outside
onDismissRequest = { showDropdown = false }
) {

Column(
modifier = modifier
.heightIn(max = 90.dp)
.verticalScroll(state = scrollState)
.border(width = 1.dp, color = Color.Gray),
horizontalAlignment = Alignment.CenterHorizontally,
) {

itemList.onEachIndexed { index, item ->
if (index != 0) {
Divider(thickness = 1.dp, color = Color.LightGray)
}
Box(
modifier = Modifier
.background(Color.Green)
.fillMaxWidth()
.clickable {
onItemClick(index)
showDropdown = !showDropdown
},
contentAlignment = Alignment.Center
) {
Text(text = item)
}
}

}
}


}
}

There are couple things I would like to point out here

First, I am using Popup instead of a Box with some zIndex. The reason for that is because the position for my other contents that I want to display in the parent view might not be directly related to that of the dropdown list. If we are using Box with zIndex, our other contents will have to go into the same parent Box as our dropdown List, ie, our composable will have the following structure.

// button
Box{...}

// parent Box
Box{
if (showDropdown) {
// dropdown list Box
Box(
modifier = Modifier.zIndex(10F)
) {...}
}

// your other contents

}

Since we don’t know how large our other contents behind the dropdown list will be, it is not really realistic to pass it in as a parameter into the DropdownList Composable.

Second, we are setting one of the Popup parameter onDismissRequest to { showDropdown = false } so that we can dismiss the dropdown list on tap outside. However, there are some down sides on onDismissRequest. If you have set the clickable for your button to be showDropdown = !showDropdown. You will notice that you cannot dismiss the list by tapping on the button anymore. This is because the onDismissRequest will triggered and showDropdown will be set to false, and then the button clickable will be triggered and showDropdown will be set back to true. You could eventually check the exact location of the tap to see whether if it is on the button or not and write your logic accordingly if you do want to dismiss the list on button tap.

Third, we are setting the verticalScroll property on the Column modifier for the dropdown list. This will enable the scroll if the contents within the Column go beyond the maximum height we specified. By setting the state to the scrollState variable we have created, we will be able to remember the scroll position and eventually scroll to the item when we show the list again.

Last but not least, we have the clickable for our dropdown list item. We will dismiss the drop down and call our onItemClick lambda that we have passed in from the parent view with parameter set to the index of the item selected, ie: onItemClick(index).

Line Up Button and List

Popup alignments are relative to the parent. In order for the button and the list to show up as one column without displacement, let’s wrap both elements within a column with horizontalAlignment = Alignment.CenterHorizontally

Column(
modifier = Modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center) {

// button
Box() {...}
// dropdown list
Box() {...}
}

You might be wondering why don’t we just move the modifier specifying the width of our button and list items to the most outside parent Column. Unfortunately , it will not work. It will not affect the list items width since those items are within Popup. In order for us to adjust the width for the items, we will have to do what we did above.

That’s it for our DropdownList Composable. Putting everything we have above together, here is how it should look like.



@Composable
fun DropdownList(itemList: List<String>, selectedIndex: Int, modifier: Modifier, onItemClick: (Int) -> Unit) {

var showDropdown by rememberSaveable { mutableStateOf(true) }
val scrollState = rememberScrollState()

Column(
modifier = Modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center) {

// button
Box(
modifier = modifier
.background(Color.Red)
.clickable { showDropdown = true },
// .clickable { showDropdown = !showDropdown },
contentAlignment = Alignment.Center
) {
Text(text = itemList[selectedIndex], modifier = Modifier.padding(3.dp))
}

// dropdown list
Box() {
if (showDropdown) {
Popup(
alignment = Alignment.TopCenter,
properties = PopupProperties(
excludeFromSystemGesture = true,
),
// to dismiss on click outside
onDismissRequest = { showDropdown = false }
) {

Column(
modifier = modifier
.heightIn(max = 90.dp)
.verticalScroll(state = scrollState)
.border(width = 1.dp, color = Color.Gray),
horizontalAlignment = Alignment.CenterHorizontally,
) {

itemList.onEachIndexed { index, item ->
if (index != 0) {
Divider(thickness = 1.dp, color = Color.LightGray)
}
Box(
modifier = Modifier
.background(Color.Green)
.fillMaxWidth()
.clickable {
onItemClick(index)
showDropdown = !showDropdown
},
contentAlignment = Alignment.Center
) {
Text(text = item,)
}
}

}
}
}
}
}

}

Use Dropdown List

Here is a quick example on how to use the Composable we have created above.


@Composable
fun ContentView() {
val itemList = listOf<String>("Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6")
var selectedIndex by rememberSaveable { mutableStateOf(0) }

var buttonModifier = Modifier.width(100.dp)

Column(
modifier = Modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// drop down list
DropdownList(itemList = itemList, selectedIndex = selectedIndex, modifier = buttonModifier, onItemClick = {selectedIndex = it})

// some other contents below the selection button and under the list
Text(text = "You have chosen ${itemList[selectedIndex]}",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(3.dp)
.fillMaxWidth()
.background(Color.LightGray),)
}
}

As I have mentioned in the beginning, we have created the selectedIndex state variable in the parent view, ie: ContentView Composable.

And to update selectedIndex with the onItemClick parameter of the DropdownList, we set the lambda to be {selectedIndex = it} so that the input parameter index that we passed into onItemClick within DropdownList will be set as the selectedIndex in the parent view.

Run your project and you should get exactly what I have showed you in the beginning.

Thank you for reading! Feel free to grab the source code for the project here from GitHub!

Have a nice day!

--

--