:android-wave: I am trying out Ballast on an app I...
# ballast
m
👋 I am trying out Ballast on an app I'm developing. I'm in a classic situation with frameworks: Ballast feels great for simple scenarios but I'm struggling with the right mental model for more complex scenarios. One such complex scenario is dealing with stateful composables like
BasicTextField()
-- details in 🧵.
I have a complex screen (multiple tabs, etc.). It includes a classic autocomplete field, which I have implemented using the latest
BasicTextField()
. 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?
c
To start, I’ll admit that I haven’t tried out the new
TextFieldState
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.
Copy code
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 }
)
As I noted, text is a special case, and such ceremony should not be necessary with other stateful composables. In other cases, you can basically treat the Ballast VM as a replacement for
remember { mutableStateOf() }
and it should work just fine
m
Fortunately, in my case, "Sync changes made by the VM back to the textFieldState" isn't needed, at least for the moment. I had considered the
LaunchedEffect
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!
c
It's probably best to observe the snapshotFlow from a LaunchedEffect, to keep that flow as close to the relevant text field as possible. You definitely don't need to fit everything into the VM, use whatever tools are available to accomplish the task. Observing the snapshot flow from a ballast
observeFlows
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
👍🏻 1