https://kotlinlang.org logo
#compose
Title
# compose
z

Zoltan Demant

10/12/2023, 7:16 AM
Is it possible to seamlessly synchronize compose text changes with an external state objects 'text' property? More details in 🧵
I have a
data class Holder(val text: String?)
(simplified) somewhere outside of compose. Ideally Id like every text change in compose to be propagated to it, instantaneously; while still allowing for fluid editing (the typing experience suffers if
holder.text
is the source of truth for the input field). Unfortunately Im "stuck" with
holder.text (String)
, otherwise a
MutableStateFlow<String>
would probably work, I think. Ive also considered making a
mutableState
in compose the source of truth, basically using
holder.text
as its initial value, then updating to reflect user input while propagating all changes upstream to
holder.text
as well. Problem here being that if the upstream
holder.text
value changes from external sources, they would be lost. Other than that, I think this solution could work; its just not very scalable; Ill eventually run into the external edits situation, hence the question. Any other ideas?
s

Stylianos Gakis

10/12/2023, 7:50 AM
Could your holder internally hold the String in MutableState instead? That would solve external changes not being reflected in the UI, and it would also make edits to it from compose instantly reflect in the state holder
z

Zoltan Demant

10/12/2023, 8:00 AM
Maybe, but Im having a hard time visualizing how the entire flow would work.. Holder is immutable, in an immutable state. Making it mutable this way would break things, probably.
In my other app, I would just debounce updates from compose ... something like
remember(holder.text) { mutableState(holder.text) }
then updating the mutableState right away, and propagating changes to
holder.text
like 0.5s later. Problem here is that input happens quickly, and I often find myself pressing continue before those 0.5s have passed. Reducing it to 0.25s or something smaller makes the input flow awkward instead, so no win win.
s

Stylianos Gakis

10/12/2023, 8:04 AM
What would it break? We definitely have some state holders like that, which are marked @Stable and have internal mutable compose State. How does the denouncing play into this here? What is the final thing you want to achieve here? Maybe that'd help me understand
z

Zoltan Demant

10/12/2023, 8:07 AM
This is the breaking point 😅 Everytime the state changes, a render pass is performed; with mutable properties in state, the states would always be equal (for changes in mutable properties).
Copy code
if (state != newState) { x }
s

Stylianos Gakis

10/12/2023, 8:07 AM
I'd just update the thing in the holder itself immediately, and if something else needs to do something in reaction to that change with a debounce, you can easily do something like
Copy code
LaunchedEffect(Unit) {
  snapshotFlow { holder.text }
    .debounce()
    .collect {
      // stuff
    }
}
Hmm, a render pass is performed, how does that affect your situation? If state changes, the UI reflects that change, isn't that fine?
Who is doing this == check that you say which is problematic? Compose will definitely be able to properly read that change, especially coming from a Stable state holder class
z

Zoltan Demant

10/12/2023, 8:13 AM
Problem is that compose is nowhere to be seen around this part of the codebase, so the state comparison is ultimately what decides if a render pass is to be performed (which then gets sent into a MutableStateFlow, that compose renders).
s

Stylianos Gakis

10/12/2023, 8:16 AM
Hmm how far from the compose layer are we talking about? We do have some places in the app where the presenter emits the new states of there are changes. But part of that state is also an object which contains a mutableState reference. So this does in fact mean that when that state changes, it doesn't make the presenter emit a new value, but it just changes it internally so to speak. This works fine, and is more or less required to make BasicTextField (or BasicTextField2) be able to work snappy
z

Zoltan Demant

10/12/2023, 8:28 AM
Its part of the in-house presenter framework I built way back! So its used both with and without compose, hence no direct integration with it yet (also, time). This means that I need the state to be "changed" for realz, since the render pass needs to happen (also, side effects from the new state need to be aware of the change, etc). Ultimately Im happy with this setup, you know how I rave about its awesomeness 😅 but for this particular case Im not sure theres a golden ticket or win/win scenario, or is there? I guess I could introduce a version parameter on the holder class, which ultimately would be used as the remember key for the mutableStates in compose. Thoughts on that?
s

Stylianos Gakis

10/12/2023, 8:44 AM
Right right then I see why this won’t work. I think your best bet is then in fact to have the state contain normal non-compose mutable state values, and in the composable itself, have a copy as MutableState for the text field to read. Then on each change, you’d have to report that back to your presenter. Will there be situations where the presenter will also want to change that text, or do all changes to it come from the text field itself? If you’d then need the presenter state change to also update that local MutableState copy, you might end up overriding new states that maybe were written really quickly while the presenter was still processing it and so on. Overall it’s never going to be perfect if you can’t hoist the entire state holding object to the model that your presenter is emitting. Which you maybe can’t do here if that presenter needs to support normal views too.
The key solution would break I think in this scenario: • Start with text “” • Change it to “a” • “a” is reported to the presenter, and it’s doing things with it • Really quickly, you change the text to “ab” • You send “ab” to the presenter again • Presenter is now done processing “a” and reports that back to the composable • “a” now replaces “ab” temporarily • (text field changed here again, if you report all changes to the presenter, now you’ve reported again a change to “a”) • “ab” is also done processing and is reported back to composable, now you have “ab” • (You send “ab” to the presenter again perhaps here) • (“a” comes back to the Composable here.) • repeat forever
z

Zoltan Demant

10/12/2023, 9:04 AM
I think we agree? Its just like the version thing I mentioned, without the version 😄 Version would just be there in order for external changes to text to be visible in compose, and I think it would be fair to expect UI elements to "just update" to reflect these changes, even if it happens in the middle of typing (this would be a rare occurence, so I dont expect that to happen a lot, or at all). As for the breakings, thats what I was seeing previously pretty much; regardless if I was using debounce or just updating holder.text and using it for UI. I think this is overall the best solution, just keeping the state in compose as SOT and updating the backing holder on every text change. When holder changes externally, compose SOT is "reset" back to the holders value, and so it goes. From my limited testing, input is super fluid and reflected in holder immediately. Hopefully nothing breaks 🤞🏽 Ill update if anything comes up. I really appreciate the discussion, this ended up working so much better than I initially anticipated!
s

Stylianos Gakis

10/12/2023, 9:14 AM
When holder changes externally, compose SOT is “reset” back to the holders value, and so it goes.
Yeap, as long as you make sure that the state changing from the input itself doesn’t create this loop, then yes it should just work I think 😄 Good luck with it! Always happy to help when I can 😊
💪🏽 1
a

ascii

10/12/2023, 11:06 AM
> Holder is immutable, in an immutable state. Making it mutable this way would break things, probably. It doesn't have to be immutable, though. It can be
@Stable
as well, with the same performance benefits, and it's what you should be doing anyway if you have mutableStates within it. Dumbed down refresher: immutable is almost the same as stable, except that it says nothing about notifying compose of changes.
I haven't read the messages since then so hopefully I'm not repeating things
s

Stylianos Gakis

10/12/2023, 12:27 PM
You are repeating things 😅
🫠 2
z

Zach Klippenstein (he/him) [MOD]

10/12/2023, 1:55 PM
On vacation and haven’t had coffee yet today so a bit slow and struggling to get the gist of the issue. Is it that where your state holder is defined is not allowed to depend on compose-runtime and thus can’t use MutableState?
s

Stylianos Gakis

10/12/2023, 2:08 PM
I think that’s probably the gist of it. Possibly combined with the part that said: “Its part of the in-house presenter framework I built … used both with and without compose … This means that I need the state to be “changed” for realz, since the render pass needs to happen (also, side effects from the new state need to be aware of the change, etc).” so my understanding here is that this framework is relying in some way on state changing in a way that you can do an equal check on it before and after to make sure they are actually different. Which prevents the usage of internally mutating state in the first place.
🤌🏽 1
z

Zoltan Demant

10/12/2023, 3:38 PM
I really wish I liked coffee. I dont know if I have anything of value to add to this discussion 😛 The approach we spoke about earlier seems to work perfectly well; even with debug builds performance is pretty much flawless despite the entire state being emitted for every letter typed (the resulting state that gets displayed is stable).
z

Zach Klippenstein (he/him) [MOD]

10/12/2023, 3:40 PM
Did you go with the explicit version number or not?
IIUC when I’ve built something similar in the past, the version/sequence number ended up being necessary for certain edge cases. But idk if this is exactly the same
s

Stylianos Gakis

10/12/2023, 3:42 PM
With BasicTextField2 won’t this be even more tricky to pull off in this way with 2 sources of truth?
z

Zach Klippenstein (he/him) [MOD]

10/12/2023, 3:44 PM
Shouldn’t be
But all the usual caveats of two SoT will still apply
👍 1
v

vide

10/12/2023, 3:50 PM
I have done this somewhat hackily over network 😅
I can read the thread more in depth tomorrow to see if there's something done differently here, I didn't have any major issues but I have a somewhat specific use case
👀 1
z

Zoltan Demant

10/12/2023, 4:05 PM
Yes, version number! Good to know that you did something similar, it's just something that came to mind spontaneously during our discussion earlier. Practically I'm doing remember(version) { mutableState(holder.text) } whereas earlier I was specifying holder.text as the key. I can imagine that basic text field 2 would work the same way.
a

ascii

10/12/2023, 5:07 PM
I really wish I liked coffee
Same! We probably have that gene that makes this and coriander etc taste far more bitter than normal.
mind blown 1
😂 1
z

Zach Klippenstein (he/him) [MOD]

10/13/2023, 12:47 PM
Careful what you wish for. I’m just a lost cause - I’ve been drinking coffee since I was like 5 years old, which is probably why I have so much anxiety 😅
z

Zoltan Demant

10/13/2023, 12:49 PM
Definitely need some more anxienty in my life 🤝🏽
🙈 1