From 88ee46df05e05b1ed1f4bf5d8d168669f1504f9f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 20:03:43 +0000 Subject: [PATCH] fix: keep back arrow visible until the exiting screen finishes leaving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit canPop() read the globally-current back-stack entry via currentBackStackEntryAsState(). A pop commits the instant it is accepted — most visibly during a predictive back-swipe, whose exit animation is long and finger-driven — so controller.currentBackStackEntry flips to the destination while the screen being dismissed is still on screen, sliding out and still composing its top bar. Evaluating canPop against the incoming destination there dropped the back arrow (and re-showed the bottom bar) before the outgoing screen had finished leaving, which is exactly the flicker seen when back-swiping to Home or a bottom-nav root. Evaluate poppability against the screen's own NavBackStackEntry — the one the NavHost provides to each destination via LocalViewModelStoreOwner — which is intrinsic to that screen and never changes for the life of its composition. The arrow now stays put until the screen itself is gone. Callers outside a NavHost destination (shell chrome, drawer) see the account-scoped owner instead of an entry and fall back to the previous globally-current behavior. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011matx1mGJ6ZyS5dS77EQUo --- .../amethyst/ui/navigation/navs/Nav.kt | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt index c8081a2cd1..1526d0be72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt @@ -28,9 +28,10 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController -import androidx.navigation.compose.currentBackStackEntryAsState import com.vitorpamplona.amethyst.ui.navigation.BOTTOM_NAV_ROOT_KEY import com.vitorpamplona.amethyst.ui.navigation.isBottomNavRoot import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -115,19 +116,35 @@ class Nav( @Composable override fun canPop(): Boolean { - // Observe the current entry as State so consumers recompose when the - // back stack settles after a navigation or back-swipe transition. - // A non-reactive read would leave a stale value behind: e.g. on - // back-swipe to Home, previousBackStackEntry is still the popping - // entry until the gesture finishes, and nothing would re-evaluate - // canPop afterwards. - val current by controller.currentBackStackEntryAsState() - val entry = current ?: return false + // Decide the back arrow / bottom-bar visibility from THIS screen's own + // back-stack entry — the one the NavHost hands to each destination + // through LocalViewModelStoreOwner — instead of the globally-current + // entry. + // + // A pop commits the moment it is accepted (most visibly during a + // predictive back-swipe, whose exit animation is long and finger-driven): + // controller.currentBackStackEntry flips to the destination while the + // screen being dismissed is still on screen, sliding out and still + // composing its top bar. Reading the global entry there re-evaluated + // canPop against the incoming destination and dropped the arrow before + // the outgoing screen had finished leaving. An entry is intrinsic to its + // screen and never changes for the life of that composition, so the arrow + // now stays put until the screen itself is gone. + // + // Outside a NavHost destination (shell chrome, drawer) the current owner + // is the account-scoped ViewModelStoreOwner, not an entry; fall back to + // the globally-current entry so those callers keep their prior behavior. + val entry = + (LocalViewModelStoreOwner.current as? NavBackStackEntry) + ?: controller.currentBackStackEntry + ?: return false + + // Hidden on tab roots (reached via the bottom nav) and on Home (the + // graph's start destination): nothing sits below either that a back + // arrow could return to. Every other entry is a push on top of Home, + // so it can always pop. if (entry.isBottomNavRoot()) return false - // Home is the graph's start destination and nothing can sit below - // it, so a back arrow there is never meaningful. - if (entry.destination.id == controller.graph.findStartDestination().id) return false - return controller.previousBackStackEntry != null + return entry.destination.id != controller.graph.findStartDestination().id } override fun popBack() {