Tomáš

    Tomáš

    1 year ago
    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.
    @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:
    @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

    mattinger

    1 year ago
    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
    SOFT_INPUT_ADJUST_RESIZE
    but it just made it even worse.
    Tomáš

    Tomáš

    1 year ago
    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.