Does Jetpack Compose offer a Material AutoComplete TextView replacement?

Viewed 3098

In the process of migrating my app to Jetpack compose, I've come to a part of my app where a TextField needs autocompletion functionality.

However, as of version 1.0.0-alpha05, I couldn't find any functionality to achieve this using the Compose API. The closest thing I've found is the DropdownMenu and DropdownMenuItem composeables, but it seems like it would be a lot of manual plumbing required to create an autocomplete menu out of these.

The obvious thing to do is just wait for future updates to Jetpack Compose, of course. But I'm wondering, has anyone who encountered a this issue in their migrations found a solution?

4 Answers

No at least till v1.0.2

so I implemented a nice working one in compose available in this gist

I also put it here:

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.window.PopupProperties


@Composable
fun TextFieldWithDropdown(
    modifier: Modifier = Modifier,
    value: TextFieldValue,
    setValue: (TextFieldValue) -> Unit,
    onDismissRequest: () -> Unit,
    dropDownExpanded: Boolean,
    list: List<String>,
    label: String = ""
) {
    Box(modifier) {
        TextField(
            modifier = Modifier
                .fillMaxWidth()
                .onFocusChanged { focusState ->
                    if (!focusState.isFocused)
                        onDismissRequest()
                },
            value = value,
            onValueChange = setValue,
            label = { Text(label) },
            colors = TextFieldDefaults.outlinedTextFieldColors()
        )
        DropdownMenu(
            expanded = dropDownExpanded,
            properties = PopupProperties(
                focusable = false,
                dismissOnBackPress = true,
                dismissOnClickOutside = true
            ),
            onDismissRequest = onDismissRequest
        ) {
            list.forEach { text ->
                DropdownMenuItem(onClick = {
                    setValue(
                        TextFieldValue(
                            text,
                            TextRange(text.length)
                        )
                    )
                }) {
                    Text(text = text)
                }
            }
        }
    }
}

How to use it

val all = listOf("aaa", "baa", "aab", "abb", "bab")

val dropDownOptions = mutableStateOf(listOf<String>())
val textFieldValue = mutableStateOf(TextFieldValue())
val dropDownExpanded = mutableStateOf(false)
fun onDropdownDismissRequest() {
    dropDownExpanded.value = false
}

fun onValueChanged(value: TextFieldValue) {
    dropDownExpanded.value = true
    textFieldValue.value = value
    dropDownOptions.value = all.filter { it.startsWith(value.text) && it != value.text }.take(3)
}

@Composable
fun TextFieldWithDropdownUsage() {
    TextFieldWithDropdown(
        modifier = Modifier.fillMaxWidth(),
        value = textFieldValue.value,
        setValue = ::onValueChanged,
        onDismissRequest = ::onDropdownDismissRequest,
        dropDownExpanded = dropDownExpanded.value,
        list = dropDownOptions.value,
        label = "Label"
    )

As of compose 1.1.0-alpha06, Compose Material now offers an ExposedDropdownMenu composable, API here, which can be used to implement a dropdown menu which facilitates the autocompletion process. The actual autocompletion logic has to be implemented yourself.

The API docs give the following usage example, for an editable field:

val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5")
var exp by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf("") }
ExposedDropdownMenuBox(expanded = exp, onExpandedChange = { exp = !exp }) {
    TextField(
        value = selectedOptionText,
        onValueChange = { selectedOptionText = it },
        label = { Text("Label") },
        trailingIcon = {
            ExposedDropdownMenuDefaults.TrailingIcon(expanded = exp)
        },
        colors = ExposedDropdownMenuDefaults.textFieldColors()
    )
    // filter options based on text field value (i.e. crude autocomplete)
    val filterOpts = options.filter { it.contains(selectedOption, ignoreCase = true) }
    if (filterOpts.isNotEmpty()) {
        ExposedDropdownMenu(expanded = exp, onDismissRequest = { exp = false }) {
            filterOpts.forEach { option ->
                DropdownMenuItem(
                    onClick = {
                        selectedOption = option
                        exp = false
                    }
                ) {
                    Text(text = option)
                }
            }
        }
    }
}

As you said, there is no such component yet. You have two options: create your own custom using DropDownMenu and BaseTextField or using hybrid xml-autocomplete and compose screen through androidx.compose.ui.platform.ComposeView

Checkout this code that I made using XML and using that layout inside compose using AndroidView. We can use this solution until it is included by default in compose.

You can customize it and style it as you want. I have personally tried it in my project and it works fine

<!-- text_input_field.xml -->
<!-- You can style your textfield here in XML with styles -->
<!-- this file should be in res/layout -->

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textfield.TextInputLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <AutoCompleteTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Label"
        android:inputType="none" />

</com.google.android.material.textfield.TextInputLayout>
// TextFieldWithDropDown.kt
// TextField with dropdown was not included by default in jetpack compose (1.0.2) and less

@Composable
fun TextFieldWithDropDown(
    items: List<String>,
    selectedValue: String?,
    modifier: Modifier = Modifier,
    onSelect: (Int) -> Unit
) {
    AndroidView(
        factory = { context ->
            val textInputLayout = TextInputLayout
                .inflate(context, R.layout.text_input_field, null) as TextInputLayout
            
            // If you need to use different styled layout for light and dark themes
            // you can create two different xml layouts one for light and another one for dark
            // and inflate the one you need here.

            val autoCompleteTextView = textInputLayout.editText as? AutoCompleteTextView
            val adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, items)
            autoCompleteTextView?.setAdapter(adapter)
            autoCompleteTextView?.setText(selectedValue, false)
            autoCompleteTextView?.setOnItemClickListener { _, _, index, _ -> onSelect(index) }
            textInputLayout
        },
        update = { textInputLayout ->
            // This block will be called when recomposition happens
            val autoCompleteTextView = textInputLayout.editText as? AutoCompleteTextView
            val adapter = ArrayAdapter(textInputLayout.context, android.R.layout.simple_list_item_1, items)
            autoCompleteTextView?.setAdapter(adapter)
            autoCompleteTextView?.setText(selectedValue, false)
        },
        modifier = modifier
    )
}
// MainActivity.kt
// It's important to use AppCompatActivity instead of ComponentActivity to get the material
// look on our XML based textfield

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setContent {
            Column {
                TextFieldWithDropDown(
                    items = listOf("One", "Two", "Three"),
                    selectedValue = "Two",
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(16.dp)
                ) {
                    // You can also set the value to a state
                    index -> println("$index was selected")
                }
            }
        }
    }
}
Related