Hey everyone :wave: I have a surprising problem re...
# compose
t
Hey everyone 👋 I have a surprising problem regarding TextField and its TextFieldValue state. I have this Composable with should work similarly to a typical search bar with suggestions that users can tap on and the content of that suggestion is added into Text field. So TextField's value can come either from the user typing things or from an external source (the suggestion being tapped). I propagate every selection and text change via
onTextChange
and persist both up somewhere in my domain logic where I 1) update the list of suggestions 2) propagate text and selection back into this composable via
descriptionWithSelection
parameter. As you can see from the sample code below, I use this
descriptionWithSelection
only to populate the initial
textFieldValueState
state but I don't know how to update that state later when a new description comes. See code examples in the thread 🧵 Thanks for any help.
🧵 4
Copy code
@Composable
fun AutocompleteTextInputEditText(
    descriptionWithSelection: TextFieldValue,
    onTextChange: (String, TextRange) -> Unit,
) {
    val textFieldValueState = remember {
        mutableStateOf(TextFieldValue(descriptionWithSelection.text, descriptionWithSelection.selection))
    }

    BasicTextField(
        value = textFieldValueState.value,
        onValueChange = { tfv ->
            textFieldValueState.value = tfv
            onTextChange(tfv.text, tfv.selection)
        }
    )
}
I couldn't find any good example of this, which is surprising given how common of a use-case this is. All samples are using a closed local loop of
TextFieldValue
without any external input.
My first idea was to remove the "local" state from the equation and simply do this:
Copy code
@Composable
fun AutocompleteTextInputEditText(
    descriptionWithSelection: TextFieldValue,
    onTextChange: (String, TextRange) -> Unit,
) {
    BasicTextField(
        value = descriptionWithSelection,
        onValueChange = { tfv ->
            onTextChange(tfv.text, tfv.selection)
        }
    )
}
This doesn't work because of two things: 1. There is a third parameter in TextFieldValue called
composition
which changes every thus
onValueChange
keeps producing values. This can be solved by persisting
composition
as well or there are other hacks such as using the local state and use
copy
method to keep the
composition
value locally persisted. But even if I solve this problem, there is a second one. 2. It comes from an asynchronous nature of
onValueChange
. It can emit an "old" value while a new one is propagated at the same time which triggers the loop again and the old value overrides the new one. (I hope it makes sense) I guess I could solve this with some upstream flows with debounce or something but that just seems more complicated than it should be.
m
This seems like something that should be a basic part of compose though. There’s already a MaterialAutoCompleteTextView in the xml view world. If there’s nothing built into compose, it seems like a big gap that is missing.
PS: I’ve found a pretty good implementation of an autocomplete here: https://github.com/pauloaapereira/Medium_JetpackCompose_AutoCompleteSearchBar
it has a few odd things, that i was able to work my way through. In particular, it doesn’t repopulate the search box after you select an item and then click back in and start deleting text. But that’s fixable. Also, there’s some issues with the soft keyboard covering up the suggestion box. I tried turning on
Copy code
SOFT_INPUT_ADJUST_RESIZE
but it just made it even worse.
t
Thanks, that's a nice example but it's a simpler case in a way that everything there is kept within the local composable world whereas in my case, I need to combine the local TextFieldValue and value coming from a flowable as a result of some domain logic which lives completely outside of @Composable context.