2

My Goal:

I'm trying to implement an autocomplete TextField in Jetpack Compose. The desired behavior is:

  1. A user starts typing in a TextField.

  2. A list of suggestions, filtered by the user's input, appears directly below the TextField.

  3. Crucially, the user must be able to continue typing to refine their search while the suggestion list is visible. The suggestion list should not be a modal popup and should not steal focus from the TextField.

What I've Tried:

My first attempt was with ExposedDropdownMenuBox. This seems like the natural choice for a dropdown, but it fails for a live-search implementation because once the menu is expanded, it becomes impossible to type in the TextField. The menu seems to grab all the focus.

Here's the anonymized code for this approach:

The following Function is used inside of an AlertDialog

// Data class for the items data class DataItem(val id: String, val displayName: String) /** * Example implementation */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchableDropdownExample() { // Example Data val allItems = remember { listOf( DataItem("1", "Apfel"), DataItem("2", "Banane"), DataItem("3", "Kirsche"), DataItem("4", "Dattel"), DataItem("5", "Holunderbeere") ) } // States for the Dropdown var isDropdownExpanded by remember { mutableStateOf(false) } var selectedDataItem by remember { mutableStateOf<DataItem?>(null)} var searchText by remember { mutableStateOf("") } val focusManager = LocalFocusManager.current // Filtert die Element-Liste basierend auf der Suchanfrage val filteredItems = remember(searchText, allItems) { if (searchText.isBlank()) { allItems } else { allItems.filter { it.displayName.contains(searchText, ignoreCase = true) } } } Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { // Das durchsuchbare Dropdown-Menü ExposedDropdownMenuBox( expanded = isDropdownExpanded, onExpandedChange = { isDropdownExpanded = !isDropdownExpanded } ) { OutlinedTextField( value = searchText, onValueChange = { searchText = it selectedDataItem = null // Auswahl zurücksetzen, wenn der Nutzer tippt isDropdownExpanded = true // Menü offen halten }, label = { Text("Element auswählen") }, placeholder = { Text("Element suchen...") }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isDropdownExpanded) }, modifier = Modifier .menuAnchor() .fillMaxWidth() ) if (filteredItems.isNotEmpty()) { ExposedDropdownMenu( expanded = isDropdownExpanded, onDismissRequest = { isDropdownExpanded = false } ) { filteredItems.forEach { item -> DropdownMenuItem( text = { Text(item.displayName) }, onClick = { selectedDataItem = item searchText = item.displayName // Textfeld mit Auswahl füllen isDropdownExpanded = false focusManager.clearFocus() } ) } } } } } } 

My Question:

How can I implement a searchable dropdown or autocomplete TextField in Jetpack Compose where the user can continue typing while the suggestion list is visible? Is there a standard Composable or an idiomatic pattern for this that avoids the focus-stealing issue I'm seeing with ExposedDropdownMenuBox?

1 Answer 1

0

ExposedDropdownMenuBox is still the way to go.

I'm not quite sure what your actual problem is, but the example code you provided does not work because you set the textfield to readOnly and didn't provide a meaningful onValueChange callback.

Furthermore it is unclear what the desired behavior should even look like. Assuming you want to force the user to select an entry from the list (just typing its full name doesn't suffice), then a possible solution could look like this:

@Composable fun BrokenSearchableDropdown( allItems: List<Item>, ) { var isExpanded by remember { mutableStateOf(false) } var selectedItem by remember { mutableStateOf<Item?>(null) } var text by remember { mutableStateOf("") } val filteredItems = allItems.filter { it.name.contains(text.trim(), true) } val focus = remember { FocusRequester() } ExposedDropdownMenuBox( expanded = isExpanded, onExpandedChange = { isExpanded = !isExpanded }, ) { OutlinedTextField( value = text, onValueChange = { selectedItem = null text = it }, label = { Text("Item") }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon( expanded = isExpanded, modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.SecondaryEditable), ) }, placeholder = { Text("Select an item") }, modifier = Modifier .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable) .focusRequester(focus) .fillMaxWidth(), ) ExposedDropdownMenu( expanded = isExpanded, onDismissRequest = { isExpanded = false }, ) { filteredItems.forEach { item -> DropdownMenuItem( text = { Text(item.name) }, onClick = { selectedItem = item text = item.name isExpanded = false focus.freeFocus() }, ) } } } } 

The following changed:

  1. readOnly is removed.

  2. The currently entered text must be stored somewhere:

    var text by remember { mutableStateOf("") } 

    Instead of deriving the OutlinedTextField's value from the selectedItem (which isn't possible if only a partial name was entered yet), this text is used instead. The "Select an item" placeholder is moved to the dedicated placeholder parameter.

    onValueChange can then be used to change this text:

    onValueChange = { selectedItem = null text = it } 

    It additionally resets selectedItem to make sure whatever was selected previously is cleared. According to the assumption above, the user must finally select an item from the list. If the user is still typing something, nothing can be selected. If you don't want this behavior, adapt as needed.

  3. With this new text you can now actually filter the list of all items:

    val filteredItems = allItems.filter { it.name.contains(text.trim(), true) } 
  4. You shouldn't use the deprecated menuAnchor without any parameters. Instead provide an additional ExposedDropdownMenuAnchorType. (This might not be available if you use a very old Compose version. You might need to update that first.)

  5. The trailingIcon should also be marked as a menuAnchor, but as a secondary only. That improves the accessibility.

  6. When selecting an Item from the list, the text should be set to the selected item name to fix proper upper/lower case and remove potential whitespace. When setting isExpanded to false you should also make sure to remove the focus so the textfield can be focused again, which sets isExpanded back to true. Depending on your use case you may not even need to do that explicitly with a FocusRequester: Maybe you want to focus the next input field instead, or hide the entire ExposedDropdownMenuBox, or navigate to another screen, and so on.

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

4 Comments

Thanks for your help. I accidentally shared the wrong code, but I've corrected that now. I tried your code and encountered the same problem again: as soon as the dropdown opens, I can no longer type in the text field. Is there a way to fix this? I have this UI within a dialog box—could that be the problem?
This code definitely works. Create a new project and just copy this code there to verify it. Then you can find out what's different in your real code by comparing the two.
As soon as I put this code into a dialog the text flield loose focus again.
The code from my answer also works without issues when in a Dialog, I cannot reproduce the problems you describe. You should probably ask a new question for that, better describing the actual issue with an minimal reproducible example. -- The current question, as asked, seems to be sufficiently answered by what I provided above.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.