Mark Murphy
09/23/2024, 1:11 PMBasicTextField()
-- details in 🧵.Mark Murphy
09/23/2024, 1:12 PMBasicTextField()
. As the user types (3+ characters and 500ms debounce), I need to update my Ballast UI state with autocomplete results obtained via a Web service call, so I can render those in a LazyColumn()
.
The BasicTextField()
is powered by a TextFieldState
. Despite the name, that's really a state holder -- it has properties that themselves are Compose State
values. I need my composable to have access to the TextFieldState
to provide to the BasicTextField()
, and I need to also observe the text changes from something that can update my Ballast UI state. I am using Ballast's BasicViewModel
(with an eye towards eventual CMP). It doesn't feel like TextFieldState
belongs in my Ballast UI state, given that it is not simple data, but I'm uncertain.
What I settled on seems a bit complex:
• My InputHandler
holds the TextFieldState
. When triggered by a particular Input
object, it uses snapshotFlow()
and observeFlows()
to map text changes to another Input
object that triggers the Web service call and eventual updateState()
.
• Rather than have the composable need to know about the InputHandler
, I inject my InputHandler
into my viewmodel (Koin), and have the viewmodel expose the TextFieldState
to the composable.
Does this approach sound reasonable?Casey Brooks
09/23/2024, 3:57 PMTextFieldState
approach to text management, and I’ve only worked with the original TextFieldValue
. Using Ballast with a TextField is a special case in itself, and the approach needed here does end up being complex, but this complexity isn’t needed for all stateful Composables. Here’s a good article that goes into depth on this unique problem, and from what I understand, TextFieldState
was the result of trying to fix the problems outlined in that article.
In general, I would agree that TextFieldState
shoudln’t be in the Ballast VM, though much of the Android docs do encourage you to hold that state as a property in the VM. Ballast VMs are strictly part of the UI layer so it’s not necessarily wrong to hold the value in the Ballast VM state, but it does just feel wrong and makes things like testing more difficult.
So to that end, I would recommend having Ballast maintain a copy of the text String from the TextFieldState
, following normal practices for managing the TextFieldState
in the composition. You would just post an additional Input to the VM in onValueChange
or have it observe a snapshotFlow
of the text.
Something like this should get you started. It does have a lot of boilerplate, especially if you have many TextFields in your app, but you could probably wrap all this boilerplate into a utility function/class to help out.
val vm = ... // Ballast VM
val uiState by vm.observeStates().collectAsState()
val textFieldState by rememberTextFieldState()
// copy the TextField text into the Ballast VM
LaunchedEffect(textFieldState) {
snapshotFlow { textFieldState.text }
.collectLatest { postInput(ExampleContract.Inputs.TextUpdated()) }
}
// Sync changes made by the VM back to the textFieldState. You may want to be a bit more clever with the updates to maintain
cursor position, etc.
LaunchedEffect(uiState.text) {
if (textFieldState.text != uiState.text) {
textFieldState.setTextAndPlaceCursorAtEnd(uiState.text)
}
}
TextField(
value = textFieldState,
onValueChange = { textFieldState = it }
)
Casey Brooks
09/23/2024, 3:58 PMremember { mutableStateOf() }
and it should work just fineMark Murphy
09/23/2024, 4:02 PMLaunchedEffect
approach and felt that would be less Ballast-y (Ballast-ish? Ballast-ified?) than what I did. But, I can switch that around.
Thanks for the feedback and for creating Ballast!Casey Brooks
09/23/2024, 4:33 PMobserveFlows
is helpful if you want to see that the text flow is attached from the Debugger, but also couples the InputHandler to Compose APIs, so it's all just a matter of tradeoffs. But I would always recommend starting with the simplest approach to solving a problem, then layering in the addition functionality only as you actually need it