15

is it possible to create Spinner with Compose framework ? Cause I was trying but looks like there is no compose method like for example to create Button :

Button(onClick = {}, modifier = Modifier .align(Alignment.CenterHorizontally) .fillMaxWidth() ) { Text(text = "Next") } 

Any advice how do create Spinner ? Do I need to use xml ?

1
  • Use androidx.compose.material.DropdownMenu and androidx.compose.material.DropdownMenuItem Commented Feb 8, 2022 at 15:59

6 Answers 6

16

Here's an example of how to create a Spinner/ComboBox/Select in Compose. https://gist.github.com/chethu/f078658ef88d138ea92ab773c7396b5d

Sign up to request clarification or add additional context in comments.

2 Comments

This link is outdated!!
9

Based on the answer above referring to the gist, I would like to propose a similar solution just in one composable but accepting an object instead of a list. In the sample I just used a Pair<String, String> that could be replaced by any object.

package at.techbee.jtx.ui.compose.elements import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Composable fun SampleSpinner( list: List<Pair<String, String>>, preselected: Pair<String, String>, onSelectionChanged: (selection: Pair<String, String>) -> Unit ) { var selected by remember { mutableStateOf(preselected) } var expanded by remember { mutableStateOf(false) } // initial value Box { Column { OutlinedTextField( value = (selected.second), onValueChange = { }, label = { Text(text = "My List") }, modifier = Modifier.fillMaxWidth(), trailingIcon = { Icon(Icons.Outlined.ArrowDropDown, null) }, readOnly = true ) DropdownMenu( modifier = Modifier.fillMaxWidth(), expanded = expanded, onDismissRequest = { expanded = false }, ) { list.forEach { entry -> DropdownMenuItem( modifier = Modifier.fillMaxWidth(), onClick = { selected = entry expanded = false }, text = { Text( text = (entry.second), modifier = Modifier.wrapContentWidth().align(Alignment.Start)) } ) } } } Spacer( modifier = Modifier .matchParentSize() .background(Color.Transparent) .padding(10.dp) .clickable( onClick = { expanded = !expanded } ) ) } } @Preview(showBackground = true) @Composable fun SampleSpinner_Preview() { MaterialTheme { val entry1 = Pair("Key1", "Entry1") val entry2 = Pair("Key2", "Entry2") val entry3 = Pair("Key3", "Entry3") SampleSpinner( listOf(entry1, entry2, entry3), preselected = entry2, onSelectionChanged = { selected -> /* do something with selected */ } ) } } 

Hope it helps!

////// EDIT /////

I have been playing around with this and I'm using now a solution that I like more. The Spacer overlay in the previous solution (also as proposed in answers before) is not really good when you would like to put a modifier on the element. So instead of the OutlinedTextField I am using a card in this solution:

import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterial3Api::class) @Composable fun SpinnerSample( list: List<MyData>, preselected: MyData, onSelectionChanged: (myData: MyData) -> Unit, modifier: Modifier = Modifier ) { var selected by remember { mutableStateOf(preselected) } var expanded by remember { mutableStateOf(false) } // initial value OutlinedCard( modifier = modifier.clickable { expanded = !expanded } ) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Top, ) { Text( text = selected.name, modifier = Modifier.weight(1f) .padding(horizontal = 16.dp, vertical = 8.dp) ) Icon(Icons.Outlined.ArrowDropDown, null, modifier = Modifier.padding(8.dp)) DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, modifier = Modifier.fillMaxWidth() // delete this modifier and use .wrapContentWidth() if you would like to wrap the dropdown menu around the content ) { list.forEach { listEntry -> DropdownMenuItem( onClick = { selected = listEntry expanded = false onSelectionChanged(selected) }, text = { Text( text = listEntry.name, modifier = Modifier //.wrapContentWidth() //optional instad of fillMaxWidth .fillMaxWidth() .align(Alignment.Start) ) }, ) } } } } } @Preview(showBackground = true) @Composable fun SpinnerSample_Preview() { MaterialTheme { val myData = listOf(MyData(0, "Apples"), MyData(1, "Bananas"), MyData(2, "Kiwis")) SpinnerSample( myData, preselected = myData.first(), onSelectionChanged = { }, modifier = Modifier.fillMaxWidth() ) } } data class MyData ( val id: Int, val name: String ) 

However, the solution with the OutlinedTextField might still be interesting as an AutoCompletion TextField.

Comments

1
//This is the sample data val specimen = ArrayList<Specimen>() specimen.add(Specimen(plantName = "Beautiful")) specimen.add(Specimen(plantName = "Paul")) specimen.add(Specimen(plantName = "Kayode")) //this is the model data class Specimen(var plantId : Int = 0, var plantName :String = "", var specimenId : String = "", var location : String = "", var description: String = "", var datePlanted: String = "", var latitude : String = "", var longitude : String = "" ) { override fun toString(): String { return "$plantName $description $location" } } //this is the composable funstion @Composable fun SpecimenSpinners(specimens: List<Specimen>) { var specimenText by remember { mutableStateOf("") } var expanded by remember { mutableStateOf(false) } val bColor = Borderline Box{ OutlinedTextField( value = (specimenText), onValueChange = { }, label = { Text( text = "Select Bank", style = TextStyle( fontFamily = Fonts.Montserrat, color = Blues ) ) }, modifier = Modifier.fillMaxWidth(), trailingIcon = { Icon(Icons.Outlined.ArrowDropDown, null) }, readOnly = true, shape = RoundedCornerShape(6.dp), leadingIcon = { IconButton(onClick = { /*TODO*/ }) { Icon( painter = painterResource(id = R.drawable.ic_baseline_business_center_24), contentDescription = "" ) } }, colors = TextFieldDefaults.outlinedTextFieldColors( backgroundColor = Color( bColor.red, bColor.green, bColor.blue, TextFieldDefaults.BackgroundOpacity ), focusedBorderColor = bColor, unfocusedBorderColor = Color( bColor.red, bColor.green, bColor.blue, TextFieldDefaults.UnfocusedIndicatorLineOpacity, ), focusedLabelColor = GreyTransparent, cursorColor = GreyTransparent ), ) DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, Modifier.fillMaxWidth() ) { specimens.forEach { specimen -> DropdownMenuItem(onClick = { expanded = false specimenText = specimen.toString() }) { Text(text = specimen.toString()) } } } Spacer( modifier = Modifier .matchParentSize() .background(Color.Transparent) .padding(10.dp) .clickable( onClick = { expanded = !expanded } ) ) } } } 

Comments

0

1 - Create Spinner.kt (who is implemented after to have many instances of Spinner)

import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.DropdownMenuItem import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue @Composable fun <T> Spinner( modifier: Modifier = Modifier, dropDownModifier: Modifier = Modifier, items: List<T>, selectedItem: T, onItemSelected: (T) -> Unit, selectedItemFactory: @Composable (Modifier, T) -> Unit, dropdownItemFactory: @Composable (T, Int) -> Unit, ) { var expanded: Boolean by remember { mutableStateOf(false) } Box(modifier = modifier.wrapContentSize(Alignment.TopStart)) { selectedItemFactory( Modifier .clickable { expanded = true }, selectedItem ) androidx.compose.material.DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, modifier = dropDownModifier ) { items.forEachIndexed { index, element -> DropdownMenuItem(onClick = { onItemSelected(items[index]) expanded = false }) { dropdownItemFactory(element, index) } } } } } 

2 - Create Instance of Spinner.kt

@Composable fun MySpinner( items: List<String>, selectedItem: String, onItemSelected: (String) -> Unit ) { Spinner( modifier = Modifier.wrapContentSize(), dropDownModifier = Modifier.wrapContentSize(), items = items, selectedItem = selectedItem, onItemSelected = onItemSelected, selectedItemFactory = { modifier, item -> Row( horizontalArrangement = Arrangement.Start, modifier = modifier .padding(8.dp) .wrapContentSize() ) { Text(item, modifier = Modifier.weight(1f)) Icon( painter = painterResource(id = R.drawable.ic_baseline_arrow_drop_down_24), contentDescription = "drop down arrow" ) } }, dropdownItemFactory = { item, _ -> Text(text = item) } ) } 

3- Use it in your code

val itemList = listOf("Item 1", "Item 2", "Item 3") var selectedItem by remember { mutableStateOf(itemList[0]) } // Default value MySpinner( items = itemList, selectedItem = selectedItem ) { selectedItem = it Log.i("TAG", "ScreenInternal: $it") } 

Comments

0

Skydoves has an implementation of his Powerspinner view for Compose here: https://github.com/skydoves/Orchestra. It is very easy to use and there are examples on the page.

Comments

0

In 2024, I think it would probably be best to use the Material 3's ExposedDropdownMenu

@OptIn(ExperimentalMaterial3Api::class) @Composable fun <T> MaterialSpinner( title: String, options: List<T>, onSelect: (option: T) -> Unit, modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } var selectedOption by remember { mutableStateOf(options[0]) } ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = it }, modifier = Modifier.then(modifier) ) { TextField( modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), value = selectedOption.toString(), onValueChange = {}, readOnly = true, singleLine = true, label = { Text(title, style = MaterialTheme.typography.labelSmall) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = ExposedDropdownMenuDefaults.textFieldColors(), ) ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { options.forEach { option -> DropdownMenuItem( text = { Text(option.toString(), style = MaterialTheme.typography.bodyLarge) }, onClick = { selectedOption = option onSelect(selectedOption) expanded = false }, contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, ) } } } } data class YourDataModel(val id: String, val name: String) @Preview(widthDp = 320, heightDp = 500, showBackground = true) @Composable fun MaterialSpinnerPreview() { val dataModels = listOf( YourDataModel(id = "123", name = "Foo"), YourDataModel(id = "321", name = "Bar") ) MaterialTheme { Column { MaterialSpinner( "Data Models", dataModels, { newValue -> Log.d("New Value = $newValue") }, Modifier.padding(10.dp) ) } } } 

Using a generic makes it pretty simple to use any type you want since toString() is always available, plus you can set the Spinner title to anything you want. The onSelect() parameter will be called whenever you set a new value for the spinner, passing in the new value for you to use however you need.

More info from the documentation

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.