One Off the Slack: How Do We Deal with Switch() State?
Csaba Szugyiczki asked:
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
onCheckedChangecallback 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.
checkedvalue is not updated immediately in the
onCheckedChangecallback, 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
Google’s Zach Klippenstein was unconvinced that this is a Compose problem:
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.
Csaba was not completely happy with this:
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.
…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
The problem lies in the animation. While we like to say that Compose UI elements
are stateless, that is not always true.
Switch(), as Csaba notes, has state,
in the form of animations.
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.
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!
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.
Csaba and Zach went back and forth a bit more as to where responsibility lies.
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
At this point, Google’s Adam Powell weighed in:
unless I’m misreading it sounds like the request is for the switch composable to be more stateful not more stateless. 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.
Csaba remained unhappy:
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.
Adam then did some more analysis and got to the heart of the problem:
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.
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.
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
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.