Android - create Spinner with Compose

Viewed 8126

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 ?

3 Answers

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.

//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 }
                        )
                )
            }
        }
    }

Related