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

Csaba Szugyiczki

01/19/2022, 12:27 PM
There is an edge-case issue we just ran into while testing our app that is about to be released in the Play Store. When the Switch is swiped by the user the
onCheckedChange
callback is called. We launch a request to our server and if it is successful only then we change the actual value that is backing the Switch’s checked state. If this happens in around 100ms the switch can stuck in an endless loop. Did anyone have a similar problem? It would be nice if we did not have to touch anything below VM layer to prevent this.
2
If the
checked
value is not updated immediately in the
onCheckedChange
callback, then the Switch is animated back to its original state. This animation is running for 100ms, so it is a timing problem within the Swipeable implementation
Reported the issue in more details, but I am looking for sensible workarounds until it is solved
z

Zach Klippenstein (he/him) [MOD]

01/19/2022, 1:25 PM
I don’t think this is necessarily an issue the UI toolkit can, or should, solve automatically. Network requests, especially on phones, will often have an RTT longer than is reasonable to update UI state directly. Good clients should account for this and be able to let the UI reflect speculative state updates like this until they have a good reason to do otherwise (network request fails or eventually returns a different final state). It’s a very bad user experience when they don’t, because even if you don’t get into an infinite loop it still feels like the checkbox is just flickering around on its own and it’s very unsettling.
c

Csaba Szugyiczki

01/19/2022, 1:31 PM
Yes I somewhat agree, but on the other hand the promise of a reactive UI framework consisting of Stateless widgets is that, it is controlled completely by the observed state of the Model/ViewModel or however we call it. In this case the inner state of the swipeable/draggable implementation is getting out of sync to the outside state for a brief period of time. We display a loader for the time the request is handled so the user knows that we are updating the settings. If we would use a checkbox for this, then we would have no issue with this approach. From the ViewModel’s point of view it should not matter if the change is coming from a CheckBox or from a Switch.
@Zach Klippenstein (he/him) [MOD] do you know any sample that is implementing the approach you suggest in a clean and elegant way? It is hard to decide where such rollback logic could be implemented that can be used generally through the whole application, with minimal impact on complexity
z

Zach Klippenstein (he/him) [MOD]

01/19/2022, 2:29 PM
I don't know of any public code samples off the top of my head, but I think you'd probably want to handle this in your repository layer. When you update the property, the repository should cache the new value locally and kick off the network request, and then if the remote update fails then propagate the new state through the repository notification mechanisms like usual.
c

Csaba Szugyiczki

01/19/2022, 2:31 PM
Hmm… for simple values like booleans it is absolutely doable, but with more complicated data it can be a real challenge to do. I am not a big fan of mixing different solutions on such low level layers. Thanks for the advice anyway!
z

Zach Klippenstein (he/him) [MOD]

01/19/2022, 2:36 PM
This is actually one of the fundamental responsibilities of the repository layer, imo, keeping local and remote state in sync in as a coherent way as possible. Some off-the-shelf services do this automatically, like I think Firebase. Your view layer should not have to worry about debouncing network responses.
c

Csaba Szugyiczki

01/19/2022, 2:43 PM
This is a philosophical question. In our approach we wait for the server response before we modify the local state. This makes sure we are in sync with the server. This is not an offline first approach but this is an easy to implement and maintain one. The one used in Firebase is better for sure, but it is much more complicated also. I would flip this conversation and ask why my Repository layer has to conform to the inner workings of my View layer or the other way around. These should be as independent and interchangeable as possible.
z

Zach Klippenstein (he/him) [MOD]

01/19/2022, 3:04 PM
I would flip this conversation and ask why my Repository layer has to conform to the inner workings of my View layer
I don’t think this has anything to do with the view layer in the first place, it’s more a matter of eventual consistency extended to the client instead of just between backend nodes. The whole purpose of the repository layer is to try to keep local and remote state in sync – if it has any responsibilities, that is it. Keeping state on multiple distributed nodes in sync is a hard problem, whether it’s between backend database nodes or between client and server. The repository pattern exists to try to isolate the logic for handling that problem from other client code.
These should be as independent and interchangeable as possible.
Yes, but that doesn’t mean different layers don’t still have basic contracts that they need to satisfy in order to make sense. In fact, this is the exact reason I’m arguing that this logic doesn’t belong in the view layer, or at least in the UI toolkit itself.
This is not an offline first approach but this is an easy to implement and maintain one. The one used in Firebase is better for sure, but it is much more complicated also.
Yep, distributed systems are complicated and hard and clients are one part of that. I understand time, staffing, and other resource constraints are a thing. But mixing responsibilities between architectural layers that aren’t intended to handle them will also put a strain on those constraints in the future – it’s a form of tech debt. A UI toolkit that tried to automatically account for lower layers behaving in unexpected ways would be much harder to reason about.
All that said, if you want to solve this with some simple debouncing in your view layer due to your own constraints and requirements, I can’t stop you. If it gets the job done, and you’re willing to accept the tech debt, great. But that doesn’t make it a good argument for something that the UI toolkit itself should automatically do.
m

myanmarking

01/19/2022, 3:17 PM
why not let the switch get to the user desired state, and if it fails display a message and revert back to the original state ? delaying the event that much will only cause problems
what we actually do in our app, we have a ‘save’ button, whether in the toolbar or a btn. If the user attemps to leave the screen without saving, we display a dialog message warning him
but i agree, the switch composable should be stateless. Should not update the value internally i guess
a

Adam Powell

01/19/2022, 3:39 PM
unless I'm misreading it sounds like the request is for the switch composable to be more state*ful* not more state*less*. Being more stateless would mean sending individual move events, maybe from 0.0-1.0f and then maybe a commit callback to lock in the value as the user lets go. Instead of a boolean, it would accept a 0-1 progress value as a parameter to form that feedback loop. That would still require you to model the state of the user request yourself, just more granularly. It wouldn't reconcile the state of the composable and the state of your server data model for you.
c

Csaba Szugyiczki

01/20/2022, 8:14 AM
Okay, if this is a behaviour that is intentional, and the framework has this “opinion” about the state being updated first without any delay, then it should be clear from the documentation at least. But I dont think it is a robust way of handling things. If the framework can be misused in such simple way, then it is a design problem in the framework I think. But thats just my 2 cents.
m

myanmarking

01/20/2022, 9:25 AM
oh. I always forget the switch can be toggled by ‘swipe’! For me, i see it as a click. Yes, @Adam Powell, that makes sense then. I wasn’t suggesting that at all. I was suggesting the action to initiate the toggle should be controlled or blocked by the user, as checkbox is for instance. But there is a move involved, so there is not really a ‘toggle’, just multiple intermediate move states
a

Adam Powell

01/20/2022, 2:50 PM
Ok it looks like we've been talking past each other a bit. The behavior described in the bug report is very much a bug; the internally triggered animation should never result in a value change callback. Only user interaction should invoke the onValueChange callback.
🙏 1
What you should be seeing in this case, minus implementation bugs, is: • User toggles the switch • onValueChange reports the requested value from the user • If you do not recompose the switch with the requested value, the switch will visually revert to the value you did compose it with • When your async confirmation returns, you recompose the switch with the requested value • The switch visually animates to reflect the value you recomposed it with This should be at most one visual revert back and catch up, with a total of one invocation of onValueChange involved.
✔️ 1
This still forms a janky user interaction even when the switch implementation bug is fixed, and the temporary workaround until a fix rolls out for the correctness bug and the long term fix for the visual jank that will still remain are going to look quite similar
✔️ 1
c

Csaba Szugyiczki

01/20/2022, 3:11 PM
@Adam Powell Thanks Adam for confirming the bug report! Yes I agree, the jank is not ideal at all, we are thinking about the approach that could work for us with these kind of auto-saved changes.
6 Views