I have a setup where my screen state is formed fro...
# compose
d
I have a setup where my screen state is formed from various sources which are gathered in the
Flow<State>
which then gets collected and rendered. When
TextField
emits a value change request — this too is considered a source of change and gets mapped/processed and then ends up in this state flow with actual
text
value calculated → rendered. Example usecase: "user inputs
foo
, ViewModel receives this request, merges it with some flags and decides to send
foo-bar
back to render in the text field". The thing is: if calculating "foo-bar" value consumes some time, then TextField state gets completely messed up, because it emits faster than it renders. In the attached video I simply press and hold the
1
key while in code I add some delay before sending it back to the
TextField
. See 🧵 for a minimum sample code. Is this something that can be done correctly?
Copy code
var state by remember { mutableStateOf(TextFieldValue()) }

val changeRequests = remember {
  MutableSharedFlow<TextFieldValue>(extraBufferCapacity = 50)
}

LaunchedEffect(Unit) {
  changeRequests.onEach { delay(100); state = it }.collect()
}

TextField(value = state, onValueChange = { changeRequests.tryEmit(it) })
a
Hi there! What is happening is related to the current internal implementation of TextField which holds 3 copies of your state. These 3 copies should be kept in sync as they represent a tight feedback loop that goes to your IME and back to the textfield. By introducing a delay you’re breaking that loop, making the state to be out of sync. The way to avoid this behaviour is return the control of the TextField immediately. You can defer the async behaviours out of this state update, and maybe updating the value generating a derived state change.
d
Thanks for explanation, I suspected something like this. Two questions: 1. I saw some mutable states in the TextField overload impl which takes a
String
, but here I use only the one with
TextFieldValue
— does it still has those 3 copies? (is this down to the
BasicTextField
?)
2. So there's no way I can receive change "request" then hold on to it and set the result back. For example I could want to implement text filtering in this way, or some other tricks which delay "rendering" of the requested state for the chance to modify it.
I mean that text would have to appear in the text field and there's no way I can substitute "foo" with "foO" without user seeing "foo" 🙂
a
1. Yep, the situation im describing applies for both TextField and BasicTextField, it’s part of internal implementation (with TextField only being the Material theme layer on top of BasicTextField)
2. would depend on which type of filtering, you can probably still do something sync that’s quick like
input.copy(text = input.text.filter { !it.isDigit() })
. For other cases in which you have to, for example, launch a coroutine and “hold the state” youre bound to have an issue
d
Understood. My main use case is that I have a flow of "change requests" from the user input, but also occasionally text could change after receiving some network response or another "dependent" field, or some calculation. And so I merge all those sources with the help of
Flow<>
(as I've described). This is quite flexible and reactive, but it seems that this "merging" itself sometimes can create enough delay to drive TextField out of sync. Thank you for explanation! If there will be some plans about improving this situation, I'd be interested to read more through whichever medium 🙂
👍 1
j
@dimsuz you should not delay the state update during typing. This should never be delayed, if you want to transform this then the transformation should happen without delay.
you wouldn't want to wait 100ms after you type to input a character 🙂
d
That
delay(...)
is to model what happens, of course I'm not having it in my code 🙂 The transformation or merging effect sometimes can create delay, see my last message above. Of course you can say "should happen without delay", but the reality is that delays can happen due to slow hardware or software.
👍 1
a
This should never be delayed, if you want to transform this then the transformation should happen without delay
Real world example would be some sort of validation which happens on Dispatcher other than Dispatchers.Main. Even without validation using Dispatchers.Default to reduce the state (e.g. in ViewModel) is enough to cause such a delay.
j
but then perhaps it should be constructed differently, I do have async things firing on typing, but they do not interfere with the typing itself. Even if you do expensive reformatting of the text, then you should probably find a way to do that without messing up the typing speed. For example you can have a state object for textValue, then you observe that from the formatter, which updates the value if it needs to. The Text element sets its value from this state, and directly updates it
Copy code
// in ViewModel
val someText = MutableStateFlow("")

init {
	viewModelScope.launch {
		someText.collectLatest {
			formatter.format(it).also { formattedText ->
				someText.update { formattedText }
			}
		}
	}

}

fun updateText(text: String) { someText.update { text } }

// in composable
val someState by someText.collectAsState()

Text(value = someState, onValueChange = { viewModel.updateText(it) })
then during typing you do not experience sluggishness and the formatting will be cancelled if you type more
you can even debounce it like this
someText.debounce(100).collectLatest { <--format-->}
d
Check out the example of merging several sources above. To recap: imagine I have •
flow1: Flow<NetworkResponse>
flow2: Flow<UserInput>
(strings) And I want to update field whenever I receive response in additiion to user input. User doesn't type, response comes → field is updated. So I roughly do this:
Copy code
val text by remember { merge(flow1.map { it.responseText } ,flow2.map { it.text}) }.collectAsState()
TextField(text, onValueChange = { flow2.emit(it) })
That
merge
above can already cause slight delays depending on the device.
j
are you then using the right dispatchers?
like main.immediate
d
It works on
main.immediate
, but not on
main
. And I'd really like to launch flow1 on Dispatchers.IO 🙂
another example would be validation as @Alexander Ivanov described above
s
Hey eng working on TextField ghost bump here
We're trying to understand this async use case precisely
I think there's two bits here and I want to see if I can tease them apart
1. Sometimes you need to do things async based on text state, such as validation. The result of this processing should never interfere with typing 2. Sometimes a network request comes back, and you need to modify text state in response to this network request. This should not interfere with typing, but it does want to modify the displayed text
For #1 I think we're exploring on an API that will resolve that and get the async write-backs out of the way
For #2, we're actually kinda confused about the use case. I can imagine something like autocomplete similar to google docs. What else exists in this space? Specifically: I want to regularly write TextField state based on async results of (text state, network state) while typing is happening Or are we missing something here - do we actually want to say something like: Option B: If the user has not typed, then I want to set TextField state
Really just trying to understand concrete use cases here to make sure we're not designing something out
d
Hi! Thanks for asking! After this thread I thought about this a bit and summed up our use case like this (gonna be a bit lenthy, to describe in detail): We have a `ViewModel`(or call it
Presenter
) which contains all the logic, no android deps, while Compose only does rendering. This
ViewModel
holds the
data class ViewState(text: String, whateverElse: Int)
for the whole screen and it also has
val state: StateFlow<ViewState>
for the renderer (Compose) to consume. On the Compose side we feed
viewState.text
to the
TextField
and
onValueChange
reports changed text to the
ViewModel
→ it updates
ViewState
and renders it. Cycle is complete. But
ViewModel
is also subscribed to other sources for text changes, let's say network request or some other system event can change it. So the
ViewModel
has something like this (in pseudo-code):
Copy code
merge(textFieldChangeFlow, networkTextFlow, systemTextFlow).collect { newText -> 
  state.update { it.copy(text = newText) 
}
// other actions coming from UI can update text too
validateButtonClickFlow.collect { 
  state.update { it.copy(text = validateAndUpdate(it.text) }
}
The crucial thing here is that we want this
ViewState
in VM to be the single source of truth for the screen, including the text field. So that screen logic in VM always works with the latest state and can update and render it at any time. So whenever text changes in Сompose, we want not only set it to the text field, but update it in VM (which in turn can cause it to render). Or VM even can decide not to update state with the new text value: this way we can implement filters and masks in VM. While also keeping logic of working with other streams there, in one place (!). But if we have the requirement that
TextField
must receive text asap, then we have to use additional
remember { }
on the compose side and
onValueChanged
should update that MutableState and then also send it to the VM. And this leaves us with two sources of truth: local mutable state in compose and ViewState consumed from VM, and while they should be in-sync most of the time, often something does cause them to go out of sync very badly when fast-typing or fast-deleting the text in the field. Perhaps it's those Flow operator roundtrips or the fact that some flows can execute on different dispatchers before being merged on Dispatchers.Main, I am not 100% sure right now. Also this case excludes ability to implement filtering/masking on VM. In my original post here I have tried to reproduce this "out-of-sync" problem with large delays, but I'm not sure that this is actually what happens, but at least it looks very similar, while not depicting out actual setup 100%.
s
Got it - thanks
So fundamentally, if you want something that’s async with typing to be the source of truth, you’ll always end up with dropped keyboard edits if they happen at-the-same-time with some other edit
This does help clarify, it sounds like your requirements are: 1. Ability for the user to always type uninterrupted (uncontrolled) 2. Ability to clobber state on some event (reset)
In the View system, we realized this pattern is usually performed by using the main thread as a mutex, so you do something like:
Copy code
mutex(ON_MAIN_THREAD)
    if(getText() == expected) setText(...)
This is all really helpful - I think we’re heading in the right direction
thanks!