Alexander Maryanovsky
09/19/2023, 6:05 PMTextField
implementation can do something like this
@Composable
fun TextField(
value: String,
onValueChanged: (String) -> Unit
) {
Box(
modifier = Modifier
.onKeyEvent { event ->
onValueChanged(value + event.keyChar)
}
) {
...
}
}
TextField
implementation uses an internal buffer which it modifies in response to events and resets when a (re)composition occurs.TextFieldValue
.Casey Brooks
09/19/2023, 6:16 PMonValueChanged
. The parent might process the value synchronously, or slowly. It might post the value to another thread for processing. It might completely ignore it. Similar, you can’t ever be sure of what value
is either. It might be hardcoded, or in a State
variable, or something else. Because of this, your TextField
composable shouldn’t assume anything about what happens outside itself.
A safer practice is to either do what the real TextField
does and maintain/update a copy of its own internal state which it can make guarantees about, or else be completely passive and only pass the keyEvent up, so that whatever parent is listening for the value can append it to the value
State itself.(Input)->Unit
lambda passed through the whole UI tree.Alexander Maryanovsky
09/19/2023, 6:22 PMonValueChanged
. The problem is in the timing of receiving feedback from onValueChanged
. By the way, using an internal buffer with onValueChanged = {}
can actually be problematic, as it will continue to fill up. Not entirely sure how TextField
deals with it.Input
up the tree can work for an application, but not for a standalone utility widget.Casey Brooks
09/19/2023, 6:36 PMvalue
and how it processes onValueChanged
, but you don’t actually know that they two are related. But if know you’re working directly with a State
, you get lots of guarantees about the value and ordering of the data within a local context.
For example, onKeyEvent
gets dispatched on the UI thread, and States
will immediately reflect whatever value you set on the same thread. So multiple quick updates to an internal State
will correctly order and update those events, since they’re in the same thread, and therefore see the same Snapshot.
But if the value is passed up to a parent composable, Compose doesn’t know that value
and onValueChanged
are meant to be part of the same snapshot from within the TextField
, so it can’t guarantee that calling onValueChanged
is synchronous with respect to value
, and so you only get a new value
upon recomposition.TextField
uses.
@Composable
fun TextField(
value: String,
onValueChanged: (String) -> Unit
) {
val _state by remember(value) { mutableStateOf(value) }
Box(
modifier = Modifier
.onKeyEvent { event ->
_state = _state + event.keyChar
onValueChanged(_state)
}
) {
...
}
}
Alexander Maryanovsky
09/19/2023, 6:41 PMonValueChanged = {}
or more realistically perhaps something like
onValueChanged = { value = it.substring(0, 10) }
trying to limit the TextField to 10 characters.Casey Brooks
09/19/2023, 6:50 PMonKeyEvent
is called, it’s fetching the latest value from the State
where it is stored and which receives the onValueChanged
update, rather than working with the composed valueAlexander Maryanovsky
09/19/2023, 6:55 PMCasey Brooks
09/19/2023, 6:57 PMTextField
and manage its own state internally. Passing the value as a lambda is definitely some non-standard Compose code, too, just like passing the State directlyZach Klippenstein (he/him) [MOD]
09/19/2023, 7:12 PMAlexander Maryanovsky
09/19/2023, 7:22 PMZach Klippenstein (he/him) [MOD]
09/19/2023, 8:09 PMTextFieldState
you can modify the state whenever and it will immediately be visible to anything using the state.