Hi facing an issue where tapping twice on a icon l...
# compose
s
Hi facing an issue where tapping twice on a icon leads to navigate called twice, whats the best way to avoid it? Am Using animated NavHost. While first navigate transition is still in progress if i click again it is leading to two navigate calls
d
This kind of problem is not unusual; it arises due to the popular idea of navigation still being based on a 'globally mutable Navigation stack'. It's somewhat funny because in other areas of development, the benefits of avoiding global mutability have long been recognised.
👍 1
An immediate answer to your problem might be to just 'debounce' user taps in your UI.
This will prevent two navigation commands being processed in quick succession.
A more robust architectural fix for it (which is more viable given that we're talking in the #compose channel) is to start treating the 'screen you should be on' as
State
.
Once your 'destination screen' is a
State
, it becomes idempotent.
Setting the screen you're on twice wouldn't matter, you're no longer 'pushing views twice onto a stack' but just reasserting your destination with (happily) no effect.
s
Thanks Chris, time to refactor and use states to hold screen
jetpack compose 1
d
Interested to hear how it works out for you; I admit I haven't completed a whole App using State for navigation myself. What I have done though is a whole App in Compose, using Jetpack Navigation and the old stack approach - and ultimately seen the futility in it.
In the end the only value the 'stack' was really adding, was the left/right transition! (The information about whether you're pushing/popping)
To embrace Compose fully, Navigation can just be a 'switch' at your top-level
@Composable
That's how I'll be looking to do my next App; and building in the knowledge of the most appropriate animation transition some other way, probably using one of Compose's
AnimatedContent
blocks.
Interesting topic and I'd be happy to be a sounding board if you want to talk through the refactor 👍
i
No need to rearchitect, just use the signal Navigation already gives you: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1629729177404900?thread_ts=1629706857.357800&cid=CJLTWPH7S
z
@darkmoon_uk Square does exactly what you propose - navigation location as state - in an app with multiple million LoC. It scales pretty well (cc @Ray Ryan )
yes black 1
👍 2
d
Good to know @Zach Klippenstein (he/him) [MOD], I would like to have refactored that last project to work that way; but the budget didn't allow - the old way was 'working'. Would be interested to know if you still do left/right transitions to indicate progression through natural sequences of screens e.g. a sign-in flow. If so how do you model-in the direction relationship to the State-based nav mechanism? I don't doubt there'd be numerous ways of doing it; just curious where you landed on that one?
r
Yes we do. In terms of how…thinking through how to phrase this w/o assuming familiarity with https://github.com/square/workflow-kotlin/.
A component that owns a back stack (say, a multi-step wizard in a modal) always emits a view model that includes the entire stack. The view code that interprets that displays only the top of the stack. On update, the view code looks to see if the new top is already part of the stack from the previous update. If so, that's a pop effect. Otherwise, push.
And b/c all that info is available to the view system, we can start using gestures like drag-right-to-pop without requiring any changes in the core component.
z
The core bits of this logic are also implemented in the much-simpler-and-smaller-scope https://github.com/rjrjr/compose-backstack
r
(Don't be fooled by my picture there, Zach wrote it. 🙂 )
w
Ray has a great exploration around persistent state, and handle cases like this (with navigation) super well. I want to call out a separate case: There are situations like button clicks that fire network requests or other things that are more transient and might want a throttle. We handled this by throttling the clicks themselves. We made an extension method that looks like this:
Copy code
fun throttle(durationMs: Long = 500, function: () -> Unit): () -> Unit = {
    val currentTime = SystemClock.uptimeMillis()
    if (lastExecutedTimestamp ?: 0 + durationMs <= currentTime) {
        lastExecutedTimestamp = currentTime
        function.invoke()
    }
}
and we use it like this:
Copy code
onClick = throttle { viewModel.onEvent(Event.ButtonTappedEvent) },
Most of our click listeners do this, so that errant or simultaneous clicks don’t result in duplicate (possibly conflicting) navigation or actions