From 51ac93da73eda8f0d335ba55118630786fdbeabd Mon Sep 17 00:00:00 2001 From: Don Turner Date: Fri, 13 Feb 2026 01:08:59 +0000 Subject: [PATCH 1/7] Migrate Jetsnack to Navigation 3 --- Jetsnack/app/build.gradle.kts | 6 +- .../com/example/jetsnack/ui/JetsnackApp.kt | 200 +++++++++--------- .../example/jetsnack/ui/components/Snacks.kt | 30 +-- .../jetsnack/ui/home/DestinationBar.kt | 4 +- .../java/com/example/jetsnack/ui/home/Home.kt | 114 ++-------- .../ui/navigation/JetsnackNavController.kt | 101 --------- .../example/jetsnack/ui/navigation/NavKeys.kt | 42 ++++ .../jetsnack/ui/snackdetail/SnackDetail.kt | 24 +-- Jetsnack/gradle/libs.versions.toml | 10 +- 9 files changed, 196 insertions(+), 335 deletions(-) delete mode 100644 Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/JetsnackNavController.kt create mode 100644 Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt diff --git a/Jetsnack/app/build.gradle.kts b/Jetsnack/app/build.gradle.kts index b66ac2188..3510bd467 100644 --- a/Jetsnack/app/build.gradle.kts +++ b/Jetsnack/app/build.gradle.kts @@ -21,6 +21,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.compose) + alias(libs.plugins.kotlin.serialization) } android { @@ -118,8 +119,11 @@ dependencies { implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.viewModelCompose) implementation(libs.androidx.lifecycle.runtime.compose) - implementation(libs.androidx.navigation.compose) implementation(libs.androidx.constraintlayout.compose) + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.lifecycle.viewmodel.navigation3) + implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.foundation) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt index b98bfae26..bbec6c432 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt @@ -20,7 +20,7 @@ package com.example.jetsnack.ui -import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionScope @@ -28,6 +28,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding @@ -35,23 +36,27 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.navArgument +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay import com.example.jetsnack.ui.components.JetsnackScaffold import com.example.jetsnack.ui.components.JetsnackSnackbar import com.example.jetsnack.ui.components.rememberJetsnackScaffoldState +import com.example.jetsnack.ui.home.Feed import com.example.jetsnack.ui.home.HomeSections import com.example.jetsnack.ui.home.JetsnackBottomBar -import com.example.jetsnack.ui.home.addHomeGraph -import com.example.jetsnack.ui.home.composableWithCompositionLocal -import com.example.jetsnack.ui.navigation.MainDestinations -import com.example.jetsnack.ui.navigation.rememberJetsnackNavController +import com.example.jetsnack.ui.home.Profile +import com.example.jetsnack.ui.home.cart.Cart +import com.example.jetsnack.ui.home.search.Search +import com.example.jetsnack.ui.navigation.CartKey +import com.example.jetsnack.ui.navigation.FeedKey +import com.example.jetsnack.ui.navigation.ProfileKey +import com.example.jetsnack.ui.navigation.SearchKey +import com.example.jetsnack.ui.navigation.SnackDetailKey +import com.example.jetsnack.ui.navigation.addHomeSection +import com.example.jetsnack.ui.navigation.addSnackDetail import com.example.jetsnack.ui.snackdetail.SnackDetail import com.example.jetsnack.ui.snackdetail.nonSpatialExpressiveSpring import com.example.jetsnack.ui.snackdetail.spatialExpressiveSpring @@ -61,110 +66,101 @@ import com.example.jetsnack.ui.theme.JetsnackTheme @Composable fun JetsnackApp() { JetsnackTheme { - val jetsnackNavController = rememberJetsnackNavController() + + val backStack = rememberNavBackStack(FeedKey) + val jetsnackScaffoldState = rememberJetsnackScaffoldState() + SharedTransitionLayout { CompositionLocalProvider( LocalSharedTransitionScope provides this, ) { - NavHost( - navController = jetsnackNavController.navController, - startDestination = MainDestinations.HOME_ROUTE, - ) { - composableWithCompositionLocal( - route = MainDestinations.HOME_ROUTE, - ) { backStackEntry -> - MainContainer( - onSnackSelected = jetsnackNavController::navigateToSnackDetail, + JetsnackScaffold( + bottomBar = { + val showBottomBar = backStack.last() !is SnackDetailKey + + AnimatedVisibility(visible = showBottomBar) { + JetsnackBottomBar( + tabs = HomeSections.entries.toTypedArray(), + currentKey = backStack.findLast { it in HomeSections.entries.map { it.route } } ?: FeedKey, + onItemClick = { navKey -> backStack.addHomeSection(navKey) }, + modifier = Modifier + .renderInSharedTransitionScopeOverlay( + zIndexInOverlay = 1f, + ) + .animateEnterExit( + enter = fadeIn(nonSpatialExpressiveSpring()) + slideInVertically( + spatialExpressiveSpring(), + ) { + it + }, + exit = fadeOut(nonSpatialExpressiveSpring()) + slideOutVertically( + spatialExpressiveSpring(), + ) { + it + }, + ), + ) + } + }, + snackbarHost = { + SnackbarHost( + hostState = it, + modifier = Modifier.systemBarsPadding(), + snackbar = { snackbarData -> JetsnackSnackbar(snackbarData) }, ) - } + }, + snackBarHostState = jetsnackScaffoldState.snackBarHostState, + ) { padding -> - composableWithCompositionLocal( - "${MainDestinations.SNACK_DETAIL_ROUTE}/" + - "{${MainDestinations.SNACK_ID_KEY}}" + - "?origin={${MainDestinations.ORIGIN}}", - arguments = listOf( - navArgument(MainDestinations.SNACK_ID_KEY) { - type = NavType.LongType - }, - ), + val modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) - ) { backStackEntry -> - val arguments = requireNotNull(backStackEntry.arguments) - val snackId = arguments.getLong(MainDestinations.SNACK_ID_KEY) - val origin = arguments.getString(MainDestinations.ORIGIN) - SnackDetail( - snackId, - origin = origin ?: "", - upPress = jetsnackNavController::upPress, - ) - } - } - } - } - } -} + val transitionSpec = fadeIn(nonSpatialExpressiveSpring()) togetherWith + fadeOut(nonSpatialExpressiveSpring()) -@Composable -fun MainContainer(modifier: Modifier = Modifier, onSnackSelected: (Long, String, NavBackStackEntry) -> Unit) { - val jetsnackScaffoldState = rememberJetsnackScaffoldState() - val nestedNavController = rememberJetsnackNavController() - val navBackStackEntry by nestedNavController.navController.currentBackStackEntryAsState() - val currentRoute = navBackStackEntry?.destination?.route - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalStateException("No SharedElementScope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalStateException("No SharedElementScope found") - JetsnackScaffold( - bottomBar = { - with(animatedVisibilityScope) { - with(sharedTransitionScope) { - JetsnackBottomBar( - tabs = HomeSections.entries.toTypedArray(), - currentRoute = currentRoute ?: HomeSections.FEED.route, - navigateToRoute = nestedNavController::navigateToBottomBarRoute, - modifier = Modifier - .renderInSharedTransitionScopeOverlay( - zIndexInOverlay = 1f, - ) - .animateEnterExit( - enter = fadeIn(nonSpatialExpressiveSpring()) + slideInVertically( - spatialExpressiveSpring(), - ) { - it - }, - exit = fadeOut(nonSpatialExpressiveSpring()) + slideOutVertically( - spatialExpressiveSpring(), - ) { - it - }, - ), + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + sharedTransitionScope = this@SharedTransitionLayout, + entryProvider = entryProvider { + entry { + Feed( + onSnackClick = backStack.addSnackDetail(), + modifier = modifier + ) + } + entry { + Cart( + onSnackClick = backStack.addSnackDetail(), + modifier = modifier + ) + } + entry { + Search( + onSnackClick = backStack.addSnackDetail(), + modifier = modifier + ) + } + entry { + Profile(modifier) + } + entry { key -> + SnackDetail( + key.snackId, + origin = key.origin, + upPress = { backStack.removeLastOrNull() }, + ) + } + }, + transitionSpec = { transitionSpec }, + popTransitionSpec = { transitionSpec }, + predictivePopTransitionSpec = { transitionSpec }, ) } } - }, - modifier = modifier, - snackbarHost = { - SnackbarHost( - hostState = it, - modifier = Modifier.systemBarsPadding(), - snackbar = { snackbarData -> JetsnackSnackbar(snackbarData) }, - ) - }, - snackBarHostState = jetsnackScaffoldState.snackBarHostState, - ) { padding -> - NavHost( - navController = nestedNavController.navController, - startDestination = HomeSections.FEED.route, - ) { - addHomeGraph( - onSnackSelected = onSnackSelected, - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding), - ) } } } -val LocalNavAnimatedVisibilityScope = compositionLocalOf { null } val LocalSharedTransitionScope = compositionLocalOf { null } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt index 028ee2b1c..4196dc849 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Snacks.kt @@ -20,6 +20,7 @@ package com.example.jetsnack.ui.components import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterExitState import androidx.compose.animation.ExperimentalSharedTransitionApi @@ -69,6 +70,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.navigation3.ui.LocalNavAnimatedContentScope import coil.compose.AsyncImage import coil.request.ImageRequest import com.example.jetsnack.R @@ -76,7 +78,6 @@ import com.example.jetsnack.model.CollectionType import com.example.jetsnack.model.Snack import com.example.jetsnack.model.SnackCollection import com.example.jetsnack.model.snacks -import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope import com.example.jetsnack.ui.LocalSharedTransitionScope import com.example.jetsnack.ui.SnackSharedElementKey import com.example.jetsnack.ui.SnackSharedElementType @@ -199,8 +200,7 @@ fun SnackItem(snack: Snack, snackCollectionId: Long, onSnackClick: (Long, String ) { val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No sharedTransitionScope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalStateException("No animatedVisibilityScope found") + val animatedContentScope = LocalNavAnimatedContentScope.current with(sharedTransitionScope) { Column( @@ -225,7 +225,7 @@ fun SnackItem(snack: Snack, snackCollectionId: Long, onSnackClick: (Long, String type = SnackSharedElementType.Image, ), ), - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = animatedContentScope, boundsTransform = snackDetailBoundsTransform, ), ) @@ -244,7 +244,7 @@ fun SnackItem(snack: Snack, snackCollectionId: Long, onSnackClick: (Long, String type = SnackSharedElementType.Title, ), ), - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = animatedContentScope, enter = fadeIn(nonSpatialExpressiveSpring()), exit = fadeOut(nonSpatialExpressiveSpring()), resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds(), @@ -268,10 +268,10 @@ private fun HighlightSnackItem( ) { val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No Scope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalStateException("No Scope found") + val animatedContentScope = LocalNavAnimatedContentScope.current + with(sharedTransitionScope) { - val roundedCornerAnimation by animatedVisibilityScope.transition + val roundedCornerAnimation by animatedContentScope.transition .animateDp(label = "rounded corner") { enterExit: EnterExitState -> when (enterExit) { EnterExitState.PreEnter -> 0.dp @@ -292,7 +292,7 @@ private fun HighlightSnackItem( type = SnackSharedElementType.Bounds, ), ), - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = animatedContentScope, boundsTransform = snackDetailBoundsTransform, clipInOverlayDuringTransition = OverlayClip( RoundedCornerShape( @@ -339,7 +339,7 @@ private fun HighlightSnackItem( type = SnackSharedElementType.Background, ), ), - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = animatedContentScope, boundsTransform = snackDetailBoundsTransform, enter = fadeIn(nonSpatialExpressiveSpring()), exit = fadeOut(nonSpatialExpressiveSpring()), @@ -374,7 +374,7 @@ private fun HighlightSnackItem( type = SnackSharedElementType.Image, ), ), - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = animatedContentScope, exit = fadeOut(nonSpatialExpressiveSpring()), enter = fadeIn(nonSpatialExpressiveSpring()), boundsTransform = snackDetailBoundsTransform, @@ -401,7 +401,7 @@ private fun HighlightSnackItem( type = SnackSharedElementType.Title, ), ), - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = animatedContentScope, enter = fadeIn(nonSpatialExpressiveSpring()), exit = fadeOut(nonSpatialExpressiveSpring()), boundsTransform = snackDetailBoundsTransform, @@ -425,7 +425,7 @@ private fun HighlightSnackItem( type = SnackSharedElementType.Tagline, ), ), - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = animatedContentScope, enter = fadeIn(nonSpatialExpressiveSpring()), exit = fadeOut(nonSpatialExpressiveSpring()), boundsTransform = snackDetailBoundsTransform, @@ -494,10 +494,10 @@ fun SnackCardPreview() { fun JetsnackPreviewWrapper(content: @Composable () -> Unit) { JetsnackTheme { SharedTransitionLayout { - AnimatedVisibility(visible = true) { + AnimatedContent(targetState = true) { _ -> CompositionLocalProvider( LocalSharedTransitionScope provides this@SharedTransitionLayout, - LocalNavAnimatedVisibilityScope provides this, + LocalNavAnimatedContentScope provides this, ) { content() } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt index b706ea8bb..b12638403 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/DestinationBar.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation3.ui.LocalNavAnimatedContentScope import com.example.jetsnack.R -import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope import com.example.jetsnack.ui.LocalSharedTransitionScope import com.example.jetsnack.ui.components.JetsnackDivider import com.example.jetsnack.ui.components.JetsnackPreviewWrapper @@ -55,7 +55,7 @@ fun DestinationBar(modifier: Modifier = Modifier) { val sharedElementScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No shared element scope") val navAnimatedScope = - LocalNavAnimatedVisibilityScope.current ?: throw IllegalStateException("No nav scope") + LocalNavAnimatedContentScope.current with(sharedElementScope) { with(navAnimatedScope) { Column( diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt index 28d1f4b92..27188225a 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt @@ -19,16 +19,10 @@ package com.example.jetsnack.ui.home import androidx.annotation.DrawableRes import androidx.annotation.FloatRange import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedContentScope -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -44,7 +38,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -67,110 +60,35 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import androidx.core.os.ConfigurationCompat -import androidx.navigation.NamedNavArgument -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavDeepLink -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import androidx.navigation.navDeepLink +import androidx.navigation3.runtime.NavKey import com.example.jetsnack.R -import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope import com.example.jetsnack.ui.components.JetsnackSurface -import com.example.jetsnack.ui.home.cart.Cart -import com.example.jetsnack.ui.home.search.Search -import com.example.jetsnack.ui.snackdetail.nonSpatialExpressiveSpring +import com.example.jetsnack.ui.navigation.CartKey +import com.example.jetsnack.ui.navigation.FeedKey +import com.example.jetsnack.ui.navigation.ProfileKey +import com.example.jetsnack.ui.navigation.SearchKey import com.example.jetsnack.ui.snackdetail.spatialExpressiveSpring import com.example.jetsnack.ui.theme.JetsnackTheme import java.util.Locale -fun NavGraphBuilder.composableWithCompositionLocal( - route: String, - arguments: List = emptyList(), - deepLinks: List = emptyList(), - enterTransition: ( - @JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition? - )? = { - fadeIn(nonSpatialExpressiveSpring()) - }, - exitTransition: ( - @JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition? - )? = { - fadeOut(nonSpatialExpressiveSpring()) - }, - popEnterTransition: ( - @JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition? - )? = - enterTransition, - popExitTransition: ( - @JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition? - )? = - exitTransition, - content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, -) { - composable( - route, - arguments, - deepLinks, - enterTransition, - exitTransition, - popEnterTransition, - popExitTransition, - ) { - CompositionLocalProvider( - LocalNavAnimatedVisibilityScope provides this@composable, - ) { - content(it) - } - } -} - -fun NavGraphBuilder.addHomeGraph(onSnackSelected: (Long, String, NavBackStackEntry) -> Unit, modifier: Modifier = Modifier) { - composable(HomeSections.FEED.route) { from -> - Feed( - onSnackClick = { id, origin -> onSnackSelected(id, origin, from) }, - modifier, - ) - } - composable(HomeSections.SEARCH.route) { from -> - Search( - onSnackClick = { id, origin -> onSnackSelected(id, origin, from) }, - modifier, - ) - } - composable( - HomeSections.CART.route, - deepLinks = listOf( - navDeepLink { uriPattern = "https://jetsnack.example.com/home/cart" }, - ), - ) { from -> - Cart( - onSnackClick = { id, origin -> onSnackSelected(id, origin, from) }, - modifier, - ) - } - composable(HomeSections.PROFILE.route) { - Profile(modifier) - } -} - -enum class HomeSections(@StringRes val title: Int, @DrawableRes val icon: Int, val route: String) { - FEED(R.string.home_feed, R.drawable.ic_home, "home/feed"), - SEARCH(R.string.home_search, R.drawable.ic_search, "home/search"), - CART(R.string.home_cart, R.drawable.ic_shopping_cart, "home/cart"), - PROFILE(R.string.home_profile, R.drawable.ic_account_circle, "home/profile"), +enum class HomeSections(@StringRes val title: Int, @DrawableRes val icon: Int, val route: NavKey) { + FEED(R.string.home_feed, R.drawable.ic_home, FeedKey), + SEARCH(R.string.home_search, R.drawable.ic_search, SearchKey), + CART(R.string.home_cart, R.drawable.ic_shopping_cart, CartKey), + PROFILE(R.string.home_profile, R.drawable.ic_account_circle, ProfileKey), } @Composable fun JetsnackBottomBar( tabs: Array, - currentRoute: String, - navigateToRoute: (String) -> Unit, + currentKey: NavKey, + onItemClick: (NavKey) -> Unit, modifier: Modifier = Modifier, color: Color = JetsnackTheme.colors.iconPrimary, contentColor: Color = JetsnackTheme.colors.iconInteractive, ) { val routes = remember { tabs.map { it.route } } - val currentSection = tabs.first { it.route == currentRoute } + val currentSection = tabs.first { it.route == currentKey } JetsnackSurface( modifier = modifier, @@ -219,7 +137,7 @@ fun JetsnackBottomBar( ) }, selected = selected, - onSelected = { navigateToRoute(section.route) }, + onSelected = { onItemClick(section.route) }, animSpec = springSpec, modifier = BottomNavigationItemPadding .clip(BottomNavIndicatorShape), @@ -422,8 +340,8 @@ private fun JetsnackBottomNavPreview() { JetsnackTheme { JetsnackBottomBar( tabs = HomeSections.entries.toTypedArray(), - currentRoute = "home/feed", - navigateToRoute = { }, + currentKey = FeedKey, + onItemClick = { }, ) } } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/JetsnackNavController.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/JetsnackNavController.kt deleted file mode 100644 index 1d16f9981..000000000 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/JetsnackNavController.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.jetsnack.ui.navigation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.lifecycle.Lifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavDestination -import androidx.navigation.NavGraph -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController - -/** - * Destinations used in the [JetsnackApp]. - */ -object MainDestinations { - const val HOME_ROUTE = "home" - const val SNACK_DETAIL_ROUTE = "snack" - const val SNACK_ID_KEY = "snackId" - const val ORIGIN = "origin" -} - -/** - * Remembers and creates an instance of [JetsnackNavController] - */ -@Composable -fun rememberJetsnackNavController(navController: NavHostController = rememberNavController()): JetsnackNavController = - remember(navController) { - JetsnackNavController(navController) - } - -/** - * Responsible for holding UI Navigation logic. - */ -@Stable -class JetsnackNavController(val navController: NavHostController) { - - // ---------------------------------------------------------- - // Navigation state source of truth - // ---------------------------------------------------------- - - fun upPress() { - navController.navigateUp() - } - - fun navigateToBottomBarRoute(route: String) { - if (route != navController.currentDestination?.route) { - navController.navigate(route) { - launchSingleTop = true - restoreState = true - // Pop up backstack to the first destination and save state. This makes going back - // to the start destination when pressing back in any other bottom tab. - popUpTo(findStartDestination(navController.graph).id) { - saveState = true - } - } - } - } - - fun navigateToSnackDetail(snackId: Long, origin: String, from: NavBackStackEntry) { - // In order to discard duplicated navigation events, we check the Lifecycle - if (from.lifecycleIsResumed()) { - navController.navigate("${MainDestinations.SNACK_DETAIL_ROUTE}/$snackId?origin=$origin") - } - } -} - -/** - * If the lifecycle is not resumed it means this NavBackStackEntry already processed a nav event. - * - * This is used to de-duplicate navigation events. - */ -private fun NavBackStackEntry.lifecycleIsResumed() = this.lifecycle.currentState == Lifecycle.State.RESUMED - -private val NavGraph.startDestination: NavDestination? - get() = findNode(startDestinationId) - -/** - * Copied from similar function in NavigationUI.kt - * - * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt - */ -private tailrec fun findStartDestination(graph: NavDestination): NavDestination { - return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph -} diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt new file mode 100644 index 000000000..ab46eb7f6 --- /dev/null +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt @@ -0,0 +1,42 @@ +package com.example.jetsnack.ui.navigation + +import androidx.compose.runtime.Composable +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +data object FeedKey : NavKey + +@Serializable +data object SearchKey : NavKey + +@Serializable +data object CartKey : NavKey + +@Serializable +data object ProfileKey : NavKey + +@Serializable +data class SnackDetailKey(val snackId: Long, val origin: String) : NavKey + +fun NavBackStack.addHomeSection(key: NavKey) { + // Remove everything except the Feed from the back stack. + removeAll { it !is FeedKey } + // Now add the key if it's not the Feed. + if (key !is FeedKey){ + add(key) + } +} + +@Composable +fun NavBackStack.addSnackDetail() : (snackId: Long, origin: String) -> Unit { + val lifecycleOwner = LocalLifecycleOwner.current + return { snackId, origin -> + if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) { + add(SnackDetailKey(snackId, origin)) + } + } +} diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt index 32cf6dd0c..985f3c69a 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt @@ -95,11 +95,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import androidx.compose.ui.unit.sp import androidx.compose.ui.util.lerp +import androidx.navigation3.ui.LocalNavAnimatedContentScope import com.example.jetsnack.R import com.example.jetsnack.model.Snack import com.example.jetsnack.model.SnackCollection import com.example.jetsnack.model.SnackRepo -import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope import com.example.jetsnack.ui.LocalSharedTransitionScope import com.example.jetsnack.ui.SnackSharedElementKey import com.example.jetsnack.ui.SnackSharedElementType @@ -147,9 +147,9 @@ fun SnackDetail(snackId: Long, origin: String, upPress: () -> Unit) { val related = remember(snackId) { SnackRepo.getRelated(snackId) } val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No Scope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalStateException("No Scope found") - val roundedCornerAnim by animatedVisibilityScope.transition + val animatedContentScope = LocalNavAnimatedContentScope.current + + val roundedCornerAnim by animatedContentScope.transition .animateDp(label = "rounded corner") { enterExit: EnterExitState -> when (enterExit) { EnterExitState.PreEnter -> 20.dp @@ -169,7 +169,7 @@ fun SnackDetail(snackId: Long, origin: String, upPress: () -> Unit) { type = SnackSharedElementType.Bounds, ), ), - animatedVisibilityScope, + animatedContentScope, clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(roundedCornerAnim)), boundsTransform = snackDetailBoundsTransform, @@ -194,7 +194,7 @@ fun SnackDetail(snackId: Long, origin: String, upPress: () -> Unit) { private fun Header(snackId: Long, origin: String) { val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalArgumentException("No Scope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current + val animatedVisibilityScope = LocalNavAnimatedContentScope.current ?: throw IllegalArgumentException("No Scope found") with(sharedTransitionScope) { @@ -250,9 +250,9 @@ private fun Header(snackId: Long, origin: String) { @Composable private fun SharedTransitionScope.Up(upPress: () -> Unit) { - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current - ?: throw IllegalArgumentException("No Scope found") - with(animatedVisibilityScope) { + val animatedContentScope = LocalNavAnimatedContentScope.current + + with(animatedContentScope) { IconButton( onClick = upPress, modifier = Modifier @@ -390,7 +390,7 @@ private fun Title(snack: Snack, origin: String, scrollProvider: () -> Int) { val minOffset = with(LocalDensity.current) { MinTitleOffset.toPx() } val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalArgumentException("No Scope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current + val animatedVisibilityScope = LocalNavAnimatedContentScope.current ?: throw IllegalArgumentException("No Scope found") with(sharedTransitionScope) { @@ -486,7 +486,7 @@ private fun Image( ) { val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No sharedTransitionScope found") - val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current + val animatedVisibilityScope = LocalNavAnimatedContentScope.current ?: throw IllegalStateException("No animatedVisibilityScope found") with(sharedTransitionScope) { @@ -550,7 +550,7 @@ private fun CartBottomBar(modifier: Modifier = Modifier) { val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No Shared scope") val animatedVisibilityScope = - LocalNavAnimatedVisibilityScope.current ?: throw IllegalStateException("No Shared scope") + LocalNavAnimatedContentScope.current ?: throw IllegalStateException("No Shared scope") with(sharedTransitionScope) { with(animatedVisibilityScope) { JetsnackSurface( diff --git a/Jetsnack/gradle/libs.versions.toml b/Jetsnack/gradle/libs.versions.toml index 9b01ac78d..e0a45324b 100644 --- a/Jetsnack/gradle/libs.versions.toml +++ b/Jetsnack/gradle/libs.versions.toml @@ -12,7 +12,8 @@ androidx-glance = "1.2.0-rc01" androidx-lifecycle = "2.8.2" androidx-lifecycle-compose = "2.10.0" androidx-lifecycle-runtime-compose = "2.10.0" -androidx-navigation = "2.9.7" +androidx-navigation3 = "1.1.0-alpha04" +androidx-lifecycle-navigation3 = "2.10.0" androidx-palette = "1.0.0" androidx-test = "1.7.0" androidx-test-espresso = "3.7.0" @@ -54,6 +55,7 @@ spotless = "8.2.1" # @keep targetSdk = "36" version-catalog-update = "1.0.1" +navigation3 = "1.0.0" [libraries] accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } @@ -98,9 +100,9 @@ androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-ru androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle-compose" } -androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } -androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" } -androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-navigation3" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" } +androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-lifecycle-navigation3" } androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } From 056afd10e336ec1805dc07d7faf551685186a900 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Fri, 13 Feb 2026 01:28:27 +0000 Subject: [PATCH 2/7] Refactor SnackDetail to use animatedContentScope --- .../jetsnack/ui/snackdetail/SnackDetail.kt | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt index 985f3c69a..88a17849a 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/snackdetail/SnackDetail.kt @@ -194,8 +194,7 @@ fun SnackDetail(snackId: Long, origin: String, upPress: () -> Unit) { private fun Header(snackId: Long, origin: String) { val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalArgumentException("No Scope found") - val animatedVisibilityScope = LocalNavAnimatedContentScope.current - ?: throw IllegalArgumentException("No Scope found") + val animatedContentScope = LocalNavAnimatedContentScope.current with(sharedTransitionScope) { val brushColors = JetsnackTheme.colors.tornado1 @@ -223,7 +222,7 @@ private fun Header(snackId: Long, origin: String) { type = SnackSharedElementType.Background, ), ), - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = animatedContentScope, boundsTransform = snackDetailBoundsTransform, enter = fadeIn(nonSpatialExpressiveSpring()), exit = fadeOut(nonSpatialExpressiveSpring()), @@ -390,8 +389,7 @@ private fun Title(snack: Snack, origin: String, scrollProvider: () -> Int) { val minOffset = with(LocalDensity.current) { MinTitleOffset.toPx() } val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalArgumentException("No Scope found") - val animatedVisibilityScope = LocalNavAnimatedContentScope.current - ?: throw IllegalArgumentException("No Scope found") + val animatedContentScope = LocalNavAnimatedContentScope.current with(sharedTransitionScope) { Column( @@ -422,7 +420,7 @@ private fun Title(snack: Snack, origin: String, scrollProvider: () -> Int) { type = SnackSharedElementType.Title, ), ), - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = animatedContentScope, boundsTransform = snackDetailBoundsTransform, ) .wrapContentWidth(), @@ -442,13 +440,13 @@ private fun Title(snack: Snack, origin: String, scrollProvider: () -> Int) { type = SnackSharedElementType.Tagline, ), ), - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = animatedContentScope, boundsTransform = snackDetailBoundsTransform, ) .wrapContentWidth(), ) Spacer(Modifier.height(4.dp)) - with(animatedVisibilityScope) { + with(animatedContentScope) { Text( text = formatPrice(snack.price), style = MaterialTheme.typography.titleLarge, @@ -486,8 +484,7 @@ private fun Image( ) { val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No sharedTransitionScope found") - val animatedVisibilityScope = LocalNavAnimatedContentScope.current - ?: throw IllegalStateException("No animatedVisibilityScope found") + val animatedContentScope = LocalNavAnimatedContentScope.current with(sharedTransitionScope) { SnackImage( @@ -502,7 +499,7 @@ private fun Image( type = SnackSharedElementType.Image, ), ), - animatedVisibilityScope = animatedVisibilityScope, + animatedVisibilityScope = animatedContentScope, exit = fadeOut(), enter = fadeIn(), boundsTransform = snackDetailBoundsTransform, @@ -549,10 +546,10 @@ private fun CartBottomBar(modifier: Modifier = Modifier) { val (count, updateCount) = remember { mutableIntStateOf(1) } val sharedTransitionScope = LocalSharedTransitionScope.current ?: throw IllegalStateException("No Shared scope") - val animatedVisibilityScope = - LocalNavAnimatedContentScope.current ?: throw IllegalStateException("No Shared scope") + val animatedContentScope = LocalNavAnimatedContentScope.current + with(sharedTransitionScope) { - with(animatedVisibilityScope) { + with(animatedContentScope) { JetsnackSurface( modifier = modifier .renderInSharedTransitionScopeOverlay(zIndexInOverlay = 4f) From 9308c4cf6ff920fb936318d88bc9e3c7da517844 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Fri, 13 Feb 2026 09:13:28 +0000 Subject: [PATCH 3/7] Improve code for getting the current HomeSection navigation key. --- .../src/main/java/com/example/jetsnack/ui/JetsnackApp.kt | 3 ++- .../app/src/main/java/com/example/jetsnack/ui/home/Home.kt | 6 +++++- .../main/java/com/example/jetsnack/ui/navigation/NavKeys.kt | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt index bbec6c432..c4daca69b 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt @@ -57,6 +57,7 @@ import com.example.jetsnack.ui.navigation.SearchKey import com.example.jetsnack.ui.navigation.SnackDetailKey import com.example.jetsnack.ui.navigation.addHomeSection import com.example.jetsnack.ui.navigation.addSnackDetail +import com.example.jetsnack.ui.navigation.currentHomeSectionKey import com.example.jetsnack.ui.snackdetail.SnackDetail import com.example.jetsnack.ui.snackdetail.nonSpatialExpressiveSpring import com.example.jetsnack.ui.snackdetail.spatialExpressiveSpring @@ -81,7 +82,7 @@ fun JetsnackApp() { AnimatedVisibility(visible = showBottomBar) { JetsnackBottomBar( tabs = HomeSections.entries.toTypedArray(), - currentKey = backStack.findLast { it in HomeSections.entries.map { it.route } } ?: FeedKey, + currentKey = backStack.currentHomeSectionKey(), onItemClick = { navKey -> backStack.addHomeSection(navKey) }, modifier = Modifier .renderInSharedTransitionScopeOverlay( diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt index 27188225a..cadf49dc1 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt @@ -75,7 +75,11 @@ enum class HomeSections(@StringRes val title: Int, @DrawableRes val icon: Int, v FEED(R.string.home_feed, R.drawable.ic_home, FeedKey), SEARCH(R.string.home_search, R.drawable.ic_search, SearchKey), CART(R.string.home_cart, R.drawable.ic_shopping_cart, CartKey), - PROFILE(R.string.home_profile, R.drawable.ic_account_circle, ProfileKey), + PROFILE(R.string.home_profile, R.drawable.ic_account_circle, ProfileKey); + + companion object { + val routes = entries.map { it.route } + } } @Composable diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt index ab46eb7f6..2bcfbbc46 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey +import com.example.jetsnack.ui.home.HomeSections import kotlinx.serialization.Serializable @Serializable @@ -31,6 +32,9 @@ fun NavBackStack.addHomeSection(key: NavKey) { } } +fun NavBackStack.currentHomeSectionKey() : NavKey = findLast { it in HomeSections.routes } + ?: error("No HomeSection key found in the back stack") + @Composable fun NavBackStack.addSnackDetail() : (snackId: Long, origin: String) -> Unit { val lifecycleOwner = LocalLifecycleOwner.current From 0cfa7199f76d1c2715b59d14048af37b30689682 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Fri, 13 Feb 2026 18:31:49 +0000 Subject: [PATCH 4/7] Fix startup crash when running in release mode --- Jetsnack/app/build.gradle.kts | 4 ++++ Jetsnack/gradle/libs.versions.toml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Jetsnack/app/build.gradle.kts b/Jetsnack/app/build.gradle.kts index 3510bd467..aabbb05d0 100644 --- a/Jetsnack/app/build.gradle.kts +++ b/Jetsnack/app/build.gradle.kts @@ -124,6 +124,10 @@ dependencies { implementation(libs.androidx.navigation3.runtime) implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.kotlinx.serialization.json) + implementation(libs.androidx.startup) + implementation(libs.androidx.work.runtime.ktx) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.foundation) diff --git a/Jetsnack/gradle/libs.versions.toml b/Jetsnack/gradle/libs.versions.toml index e0a45324b..22a564e1f 100644 --- a/Jetsnack/gradle/libs.versions.toml +++ b/Jetsnack/gradle/libs.versions.toml @@ -15,6 +15,7 @@ androidx-lifecycle-runtime-compose = "2.10.0" androidx-navigation3 = "1.1.0-alpha04" androidx-lifecycle-navigation3 = "2.10.0" androidx-palette = "1.0.0" +androidx-startup = "1.2.0" androidx-test = "1.7.0" androidx-test-espresso = "3.7.0" androidx-test-ext-junit = "1.3.0" @@ -56,6 +57,7 @@ spotless = "8.2.1" targetSdk = "36" version-catalog-update = "1.0.1" navigation3 = "1.0.0" +work = "2.9.1" [libraries] accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } @@ -107,6 +109,7 @@ androidx-palette = { module = "androidx.palette:palette", version.ref = "android androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-startup = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" } androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" } @@ -122,6 +125,7 @@ androidx-wear-compose-navigation = { module = "androidx.wear.compose:compose-nav androidx-wear-compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "androidx-wear-compose" } androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" } androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window" } +androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } core-jdk-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "jdkDesugar" } dagger-hiltandroidplugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" } From b02f45889154221c37ec8a0c034e2b41b50ce0a7 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Fri, 13 Feb 2026 18:57:32 +0000 Subject: [PATCH 5/7] Specify transitions on the bottom bar directly. --- .../com/example/jetsnack/ui/JetsnackApp.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt index c4daca69b..e76d316af 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt @@ -79,7 +79,19 @@ fun JetsnackApp() { bottomBar = { val showBottomBar = backStack.last() !is SnackDetailKey - AnimatedVisibility(visible = showBottomBar) { + AnimatedVisibility( + visible = showBottomBar, + enter = fadeIn(nonSpatialExpressiveSpring()) + slideInVertically( + spatialExpressiveSpring(), + ) { + it + }, + exit = fadeOut(nonSpatialExpressiveSpring()) + slideOutVertically( + spatialExpressiveSpring(), + ) { + it + } + ) { JetsnackBottomBar( tabs = HomeSections.entries.toTypedArray(), currentKey = backStack.currentHomeSectionKey(), @@ -87,18 +99,6 @@ fun JetsnackApp() { modifier = Modifier .renderInSharedTransitionScopeOverlay( zIndexInOverlay = 1f, - ) - .animateEnterExit( - enter = fadeIn(nonSpatialExpressiveSpring()) + slideInVertically( - spatialExpressiveSpring(), - ) { - it - }, - exit = fadeOut(nonSpatialExpressiveSpring()) + slideOutVertically( - spatialExpressiveSpring(), - ) { - it - }, ), ) } @@ -128,19 +128,19 @@ fun JetsnackApp() { entry { Feed( onSnackClick = backStack.addSnackDetail(), - modifier = modifier + modifier = modifier, ) } entry { Cart( onSnackClick = backStack.addSnackDetail(), - modifier = modifier + modifier = modifier, ) } entry { Search( onSnackClick = backStack.addSnackDetail(), - modifier = modifier + modifier = modifier, ) } entry { From 3c9c8cbf14335452aeb544f049e9f0dfc24658b1 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Fri, 13 Feb 2026 18:58:34 +0000 Subject: [PATCH 6/7] Fix for Widget hanging do to WorkManager classes being removed by proguard. --- Jetsnack/app/proguard-rules.pro | 5 ++++- Jetsnack/gradle/libs.versions.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Jetsnack/app/proguard-rules.pro b/Jetsnack/app/proguard-rules.pro index f8f76182d..de9c52a5c 100644 --- a/Jetsnack/app/proguard-rules.pro +++ b/Jetsnack/app/proguard-rules.pro @@ -34,4 +34,7 @@ -dontwarn org.openjsse.javax.net.ssl.SSLSocket -dontwarn org.openjsse.net.ssl.OpenJSSE --keep class androidx.compose.ui.platform.AndroidCompositionLocals_androidKt { *; } \ No newline at end of file +-keep class androidx.compose.ui.platform.AndroidCompositionLocals_androidKt { *; } + +-keep class androidx.work.** { *; } +-keep class androidx.work.impl.** { *; } diff --git a/Jetsnack/gradle/libs.versions.toml b/Jetsnack/gradle/libs.versions.toml index 22a564e1f..0f47636a9 100644 --- a/Jetsnack/gradle/libs.versions.toml +++ b/Jetsnack/gradle/libs.versions.toml @@ -57,7 +57,7 @@ spotless = "8.2.1" targetSdk = "36" version-catalog-update = "1.0.1" navigation3 = "1.0.0" -work = "2.9.1" +work = "2.10.0" [libraries] accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } From 967f393ccd1a9aeaf4d38b0fb2bd705ad9e76093 Mon Sep 17 00:00:00 2001 From: Don Turner Date: Fri, 13 Feb 2026 18:59:30 +0000 Subject: [PATCH 7/7] Fix formatting issues --- .../com/example/jetsnack/ui/JetsnackApp.kt | 4 ++-- .../java/com/example/jetsnack/ui/home/Home.kt | 3 ++- .../example/jetsnack/ui/navigation/NavKeys.kt | 22 ++++++++++++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt index e76d316af..7a69bf448 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt @@ -90,7 +90,7 @@ fun JetsnackApp() { spatialExpressiveSpring(), ) { it - } + }, ) { JetsnackBottomBar( tabs = HomeSections.entries.toTypedArray(), @@ -118,7 +118,7 @@ fun JetsnackApp() { .consumeWindowInsets(padding) val transitionSpec = fadeIn(nonSpatialExpressiveSpring()) togetherWith - fadeOut(nonSpatialExpressiveSpring()) + fadeOut(nonSpatialExpressiveSpring()) NavDisplay( backStack = backStack, diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt index cadf49dc1..0c19418e5 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/Home.kt @@ -75,7 +75,8 @@ enum class HomeSections(@StringRes val title: Int, @DrawableRes val icon: Int, v FEED(R.string.home_feed, R.drawable.ic_home, FeedKey), SEARCH(R.string.home_search, R.drawable.ic_search, SearchKey), CART(R.string.home_cart, R.drawable.ic_shopping_cart, CartKey), - PROFILE(R.string.home_profile, R.drawable.ic_account_circle, ProfileKey); + PROFILE(R.string.home_profile, R.drawable.ic_account_circle, ProfileKey), + ; companion object { val routes = entries.map { it.route } diff --git a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt index 2bcfbbc46..9690c848c 100644 --- a/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt +++ b/Jetsnack/app/src/main/java/com/example/jetsnack/ui/navigation/NavKeys.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.example.jetsnack.ui.navigation import androidx.compose.runtime.Composable @@ -27,16 +43,16 @@ fun NavBackStack.addHomeSection(key: NavKey) { // Remove everything except the Feed from the back stack. removeAll { it !is FeedKey } // Now add the key if it's not the Feed. - if (key !is FeedKey){ + if (key !is FeedKey) { add(key) } } -fun NavBackStack.currentHomeSectionKey() : NavKey = findLast { it in HomeSections.routes } +fun NavBackStack.currentHomeSectionKey(): NavKey = findLast { it in HomeSections.routes } ?: error("No HomeSection key found in the back stack") @Composable -fun NavBackStack.addSnackDetail() : (snackId: Long, origin: String) -> Unit { +fun NavBackStack.addSnackDetail(): (snackId: Long, origin: String) -> Unit { val lifecycleOwner = LocalLifecycleOwner.current return { snackId, origin -> if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {