dimsuz
07/21/2022, 5:16 PMFlow<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?dimsuz
07/21/2022, 5:16 PMvar 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) })
Ale Stamato
07/21/2022, 6:08 PMdimsuz
07/21/2022, 6:16 PMString
, but here I use only the one with TextFieldValue
— does it still has those 3 copies? (is this down to the BasicTextField
?)dimsuz
07/21/2022, 6:18 PMdimsuz
07/21/2022, 6:19 PMAle Stamato
07/21/2022, 6:41 PMAle Stamato
07/21/2022, 6:42 PMinput.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 issuedimsuz
07/21/2022, 10:42 PMFlow<>
(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 🙂Joost Klitsie
07/22/2022, 11:23 AMJoost Klitsie
07/22/2022, 11:25 AMdimsuz
07/22/2022, 11:41 AMdelay(...)
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.Alexander Ivanov
07/22/2022, 11:42 AMThis should never be delayed, if you want to transform this then the transformation should happen without delayReal 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.
Joost Klitsie
07/22/2022, 11:45 AMJoost Klitsie
07/22/2022, 11:48 AM// 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) })
Joost Klitsie
07/22/2022, 11:49 AMJoost Klitsie
07/22/2022, 11:49 AMJoost Klitsie
07/22/2022, 11:49 AMsomeText.debounce(100).collectLatest { <--format-->}
dimsuz
07/22/2022, 11:51 AMflow1: 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:
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.Joost Klitsie
07/22/2022, 11:52 AMJoost Klitsie
07/22/2022, 11:52 AMdimsuz
07/22/2022, 11:55 AMmain.immediate
, but not on main
. And I'd really like to launch flow1 on Dispatchers.IO 🙂dimsuz
07/22/2022, 11:57 AMSean McQuillan [G]
08/12/2022, 4:20 PMSean McQuillan [G]
08/12/2022, 4:21 PMSean McQuillan [G]
08/12/2022, 4:23 PMSean McQuillan [G]
08/12/2022, 4:24 PMSean McQuillan [G]
08/12/2022, 4:24 PMSean McQuillan [G]
08/12/2022, 4:27 PMSean McQuillan [G]
08/12/2022, 4:27 PMdimsuz
08/12/2022, 6:38 PMPresenter
) 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):
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%.Sean McQuillan [G]
08/15/2022, 3:55 PMSean McQuillan [G]
08/15/2022, 3:56 PMSean McQuillan [G]
08/15/2022, 3:57 PMSean McQuillan [G]
08/15/2022, 3:57 PMmutex(ON_MAIN_THREAD)
if(getText() == expected) setText(...)
Sean McQuillan [G]
08/15/2022, 3:58 PMSean McQuillan [G]
08/15/2022, 3:58 PM