So, after a lot of pretty words and wondering why the compose Gods made it so hard, I managed to create an autocomplete field that does the following:
- Filter items as you type (even after the menu pops up)
- Does not open and closes just by clicking on it, but only filters by the text inserted
- Does not closes the Keyboard
- Does not pop back up when clicking an already completed Field, suggesting one option(the text already there)
Below is my code, with some particularities to my app, but with comments on the important aspects. Can be modified as you wish from a filtering perspective, and design.
Note: I am using "androidx.compose.material3:material3:1.3.0-beta04" when writing this answer. Check the most recent version available when you find this comment.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AutocompleteTextField(
value: TextFieldValue,
onValueChanged: (TextFieldValue) -> Unit,
hint: String,
suggestionsList: Set<String>,
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
capitalization = KeyboardCapitalization.Words
),
onDeleteItemClick: (String) -> Unit,
maxNumberOfCharacters: Int = 35,
dropdownColor: Color = MaterialTheme.colorScheme.surface
) {
//Get results filtered as input changes
val filteredSuggestions = remember(value) {
if (value.text.isNotEmpty()) {
suggestionsList.filter {
it.startsWith(prefix = value.text, ignoreCase = true)
}.toSet()
} else {
emptyList()
}
}
//Decide if the Dropdown is visible based on the filtered results and inserted text
var expanded by remember(
filteredSuggestions
) {
mutableStateOf(
filteredSuggestions.isNotEmpty()
&& value.text.isNotBlank() // Don't show menu when just selecting text
&& !filteredSuggestions.contains(value.text) // Don't show as a suggestion the full text already there
)
}
ExposedDropdownMenuBox(
modifier = modifier,
expanded = expanded,
onExpandedChange = {
//The expansion is triggered by the suggestion list and text changes, not by click
}
) {
MyCustomTextField(
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (!it.isFocused) {
//Closes the dropdown when user moves to another field
expanded = false
}
}
//Makes it possible to keep writing once the popup shows up.
//Requires material3 version at least "1.3.0-beta04"
//Without this line you get a FocusRequester missing error
.menuAnchor(type = MenuAnchorType.PrimaryEditable),
value = value,
onValueChanged = onValueChanged,
hint = hint,
keyboardOptions = keyboardOptions,
maxNumberOfCharacters = maxNumberOfCharacters
)
if (filteredSuggestions.isNotEmpty()) {
//Using DropdownMenu instead ExposedDropdownMenu because we can use the properties
// to stop the keyboard from closing. As of writing this code using material3 1.3.0-beta04
//I could not find a way to stop the keyboard from closing using ExposedDropdownMenu
DropdownMenu(
modifier = Modifier.exposedDropdownSize(), //Make the dropdown as big as the ExposedDropdownMenuBox
containerColor = dropdownColor,
expanded = expanded,
onDismissRequest = {
//Closes the popup when used clicks outside
expanded = false
},
properties = PopupProperties(focusable = false) //Stops the dropdown from closing the keyboard
) {
filteredSuggestions.forEach { name ->
DropdownMenuItem(
onClick = {
expanded = false
onValueChanged(TextFieldValue(name))
},
text = {
MyCustomAutocompleteDropdownItem(
modifier = Modifier.fillMaxWidth(),
itemName = name,
onDeleteItemClick = onDeleteItemClick
)
}
)
}
}
}
}
}
Hope this helps you out!
Cheers!