fix(nav): keep Home below tab roots, hide back arrow via per-entry flag
Previous attempt cleared Home from the back stack on every bottom-nav tap, which broke back-swipe: the user expects swipe-back from any tab to return to Home, and swipe-back from Home to leave the app. Restore that contract while still hiding the back arrow on tab roots: - navBottomBar pops up to (but not including) Route.Home, so sibling tabs are cleared while Home stays at the bottom of the stack. - The new entry is stamped with BOTTOM_NAV_ROOT_KEY on its savedStateHandle. - Nav.canPop returns false when the current entry carries that flag, even though previousBackStackEntry (Home) is non-null. Home itself is the graph's start destination and has no previous entry, so canPop returns false there without needing the flag. https://claude.ai/code/session_01PrirRcL7g8iX7vTqqLTkBS
This commit is contained in:
@@ -41,6 +41,13 @@ const val SKIP_SLIDE_ANIMATION_KEY = "skipSlideAnimation"
|
||||
|
||||
fun NavBackStackEntry.skipsSlideAnimation(): Boolean = savedStateHandle.get<Boolean>(SKIP_SLIDE_ANIMATION_KEY) == true
|
||||
|
||||
// Per-entry hint stamped by Nav.navBottomBar marking that the entry was
|
||||
// reached via a bottom-nav tab. Used by Nav.canPop so top bars can hide
|
||||
// the back arrow on tab roots even though Home sits below them.
|
||||
const val BOTTOM_NAV_ROOT_KEY = "bottomNavRoot"
|
||||
|
||||
fun NavBackStackEntry.isBottomNavRoot(): Boolean = savedStateHandle.get<Boolean>(BOTTOM_NAV_ROOT_KEY) == true
|
||||
|
||||
inline fun <reified T : Any> NavGraphBuilder.composableFromEnd(noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit) {
|
||||
composable<T>(
|
||||
enterTransition = { if (targetState.skipsSlideAnimation()) null else slideInHorizontallyFromEnd },
|
||||
|
||||
@@ -24,9 +24,10 @@ import android.annotation.SuppressLint
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import com.vitorpamplona.amethyst.ui.navigation.BOTTOM_NAV_ROOT_KEY
|
||||
import com.vitorpamplona.amethyst.ui.navigation.SKIP_SLIDE_ANIMATION_KEY
|
||||
import com.vitorpamplona.amethyst.ui.navigation.isBottomNavRoot
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.getRouteWithArguments
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -79,22 +80,28 @@ class Nav(
|
||||
override fun navBottomBar(route: Route) {
|
||||
navigationScope.launch {
|
||||
controller.navigate(route) {
|
||||
// Clear the back stack down to and including the graph's start
|
||||
// destination so a bottom-nav tap leaves only the new route in
|
||||
// the stack. Without inclusive=true, Home would remain below
|
||||
// the new tab and canPop() would wrongly show a back arrow.
|
||||
popUpTo(controller.graph.findStartDestination().id) {
|
||||
inclusive = true
|
||||
// Clear sibling bottom-nav entries but keep Home (the start
|
||||
// destination) below, so back-swipe from any tab returns to
|
||||
// Home and back-swipe from Home leaves the app.
|
||||
popUpTo(Route.Home) {
|
||||
inclusive = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
// Stamp the entry so composableFromEnd's transition lambdas can
|
||||
// skip the slide animation when this entry enters or exits.
|
||||
controller.getBackStackEntry(route).savedStateHandle[SKIP_SLIDE_ANIMATION_KEY] = true
|
||||
val entry = controller.getBackStackEntry(route)
|
||||
// Skip the horizontal slide for composableFromEnd transitions.
|
||||
entry.savedStateHandle[SKIP_SLIDE_ANIMATION_KEY] = true
|
||||
// Mark this entry as a tab root so canPop hides the back arrow
|
||||
// even though Home sits below it.
|
||||
entry.savedStateHandle[BOTTOM_NAV_ROOT_KEY] = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun canPop(): Boolean = controller.previousBackStackEntry != null
|
||||
override fun canPop(): Boolean {
|
||||
val current = controller.currentBackStackEntry ?: return false
|
||||
if (current.isBottomNavRoot()) return false
|
||||
return controller.previousBackStackEntry != null
|
||||
}
|
||||
|
||||
override fun popBack() {
|
||||
navigationScope.launch {
|
||||
|
||||
Reference in New Issue
Block a user