android - Type-Safe Navigation with Nested Graphs, NavigationBar, and Inclusive PopUp in Jetpack Compose - Stack Overflow

Is there a proper example of type-safe navigation with nested graphs where one of the graphs includes a

Is there a proper example of type-safe navigation with nested graphs where one of the graphs includes a NavigationBar with its own screens, and an auth/splash/onboarding screen is removed from the back stack using inclusive = true, all without duplicating rememberNavController/NavHost?

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            IncomeTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    NavigationHost()
                }
            }
        }
    }
}

@Serializable
sealed class Graph {
    @Serializable
    object OnboardingGraph : Graph()

    @Serializable
    object MainGraph : Graph()
}

@Serializable
sealed class OnboardingGraph {
    @Serializable
    object OnboardingScreen : OnboardingGraph()
}

@Serializable
sealed class MainGraph {
    @Serializable
    object Home : MainGraph()
    @Serializable
    object Notification : MainGraph()
    @Serializable
    object History : MainGraph()
    @Serializable
    object Settings : MainGraph()
}

enum class TopLevelRoute(
    val route: Any,
    @StringRes val label: Int,
    @DrawableRes val iconFill: Int
) {
    HOME(
        MainGraph.Home,
        R.string.tab_home,
        R.drawable.baseline_home_24
    ),
    NOTIFICATION(
        MainGraph.Notification,
        R.string.tab_notification,
        R.drawable.baseline_notifications_24
    ),
    HISTORY(
        MainGraph.History,
        R.string.tab_history,
        R.drawable.baseline_history_edu_24
    ),
    SETTINGS(
        MainGraph.Settings,
        R.string.tab_settings,
        R.drawable.baseline_settings_24
    );
}

@Composable
fun BottomNavigationBar(
    navController: NavHostController,
    currentDestination: NavDestination?
) {

    NavigationBar {
        TopLevelRoute.entries.forEach { navigationItem ->
            NavigationBarItem(
                selected = currentDestination?.hierarchy?.any {
                    it.hasRoute(navigationItem.route::class)
                } == true,
                label = {
                    Text(text = stringResource(navigationItem.label))
                },
                icon = {
                    Icon(
                        painterResource(navigationItem.iconFill),
                        contentDescription = stringResource(navigationItem.label)
                    )
                },
                onClick = {
                    navController.navigate(navigationItem.route) {
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                }
            )
        }
    }
}

@Composable
private fun NavigationHost() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = Graph.OnboardingGraph,
        enterTransition = { EnterTransition.None },
        exitTransition = { ExitTransition.None }
    ) {
        onboardingNavGraph(navController)

        composable<MainGraph.Home> {
            MainScreenUI(navController = navController)
        }
    }
}

fun NavGraphBuilder.onboardingNavGraph(
    navController: NavController
) {
    navigation<Graph.OnboardingGraph>(startDestination = OnboardingGraph.OnboardingScreen) {
        composable<OnboardingGraph.OnboardingScreen> {
            OnboardingScreen(
                onFinished = {
                    navController.navigate(MainGraph.Home) {
                        popUpTo(OnboardingGraph.OnboardingScreen) {
                            inclusive = true
                        }
                    }
                }
            )
        }
    }
}

@Composable
fun MainScreenUI(
    navController: NavHostController
) {
    val navController = rememberNavController()

    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        bottomBar = {
            BottomNavigationBar(
                navController = navController,
                currentDestination = currentDestination
            )
        }
    ) { paddingValues ->
        NavHost(
            modifier = Modifier.padding(paddingValues),
            navController = navController,
            startDestination = Graph.MainGraph,
            enterTransition = { EnterTransition.None },
            exitTransition = { ExitTransition.None }
        ) {
            mainNavGraph(navController)
        }
    }
}

fun NavGraphBuilder.mainNavGraph(
    navController: NavController
) {
    navigation<Graph.MainGraph>(startDestination = MainGraph.Home) {
        composable<MainGraph.Home> {
            HomeScreen { /* TODO */ }
        }
        composable<MainGraph.Notification> {
            NotificationScreen { /* TODO */ }
        }
        composable<MainGraph.History> {
            HistoryScreen { /* TODO */ }
        }
        composable<MainGraph.Settings> {
            SettingsScreen { /* TODO */ }
        }
    }
}

Also tried using a single NavHost with two nested graphs, but calling:

navController.navigate(MainGraph.Home) {
    popUpTo(OnboardingGraph.OnboardingScreen) {
        inclusive = true
    }
}

results in the error: "Ignoring popBackStack to destination XXXXXX as it was not found on the current back stack", as well as screen flickering when navigating to the same screen again. The error occurs because, when tapping on a bottom navigation item, the popUpTo method is called with the start destination ID, which is determined using navController.graph.findStartDestination().id. However, the start destination of the graph remains the onboarding screen, which was already removed from the back stack after it was completed. As a result, an attempt is made to "pop up" to a screen that no longer exists in the stack. (In the case of attempting to use two graph builders with a single NavHost) At the moment, I see a solution by setting the MainGraph\Home screen as the destination in the bottom navigation instead of using findStartDestination(). However, I'm not sure if this is the best practice.


@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            IncomeTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    MainScreenUI()
                }
            }
        }
    }
}

@Serializable
sealed class Graph {
    @Serializable
    object OnboardingGraph : Graph()

    @Serializable
    object MainGraph : Graph()
}

@Serializable
sealed class OnboardingGraph {
    @Serializable
    object OnboardingScreen : OnboardingGraph()
}

@Serializable
sealed class MainGraph {
    @Serializable
    object Home : MainGraph()
    @Serializable
    object Notification : MainGraph()
    @Serializable
    object History : MainGraph()
    @Serializable
    object Settings : MainGraph()
}

enum class TopLevelRoute(
    val route: Any,
    @StringRes val label: Int,
    @DrawableRes val iconFill: Int
) {
    HOME(
        MainGraph.Home,
        R.string.tab_home,
        R.drawable.baseline_home_24
    ),
    NOTIFICATION(
        MainGraph.Notification,
        R.string.tab_notification,
        R.drawable.baseline_notifications_24
    ),
    HISTORY(
        MainGraph.History,
        R.string.tab_history,
        R.drawable.baseline_history_edu_24
    ),
    SETTINGS(
        MainGraph.Settings,
        R.string.tab_settings,
        R.drawable.baseline_settings_24
    );
}

@Composable
fun MainScreenUI() {
    val navController = rememberNavController()
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        bottomBar = {
            // distance check to exclude non-incoming screens
            BottomNavigationBar(
                navController = navController,
                currentDestination = currentDestination
            )
        }
    ) { paddingValues ->
        NavHost(
            modifier = Modifier.padding(paddingValues),
            navController = navController,
            startDestination = Graph.OnboardingGraph,
            enterTransition = { EnterTransition.None },
            exitTransition = { ExitTransition.None }
        ) {
            onboardingNavGraph(navController)
            mainNavGraph(navController)
        }
    }
}

@Composable
fun BottomNavigationBar(
    navController: NavHostController,
    currentDestination: NavDestination?
) {
    NavigationBar {
        TopLevelRoute.entries.forEach { navigationItem ->
            NavigationBarItem(
                selected = currentDestination?.hierarchy?.any {
                    it.hasRoute(navigationItem.route::class)
                } == true,
                label = {
                    Text(text = stringResource(navigationItem.label))
                },
                icon = {
                    Icon(
                        painterResource(navigationItem.iconFill),
                        contentDescription = stringResource(navigationItem.label)
                    )
                },
                onClick = {
                    navController.navigate(navigationItem.route) {
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                }
            )
        }
    }
}

fun NavGraphBuilder.onboardingNavGraph(
    navController: NavController
) {
    navigation<Graph.OnboardingGraph>(startDestination = OnboardingGraph.OnboardingScreen) {
        composable<OnboardingGraph.OnboardingScreen> {
            OnboardingScreen(
                onFinished = {
                    navController.navigate(MainGraph.Home) {
                        popUpTo(OnboardingGraph.OnboardingScreen) {
                            inclusive = true
                        }
                    }
                }
            )
        }
    }
}

fun NavGraphBuilder.mainNavGraph(
    navController: NavController
) {
    navigation<Graph.MainGraph>(startDestination = MainGraph.Home) {
        composable<MainGraph.Home> {
            HomeScreen { /* TODO */ }
        }
        composable<MainGraph.Notification> {
            NotificationScreen { /* TODO */ }
        }
        composable<MainGraph.History> {
            HistoryScreen { /* TODO */ }
        }
        composable<MainGraph.Settings> {
            SettingsScreen { /* TODO */ }
        }
    }
}

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744735608a4590726.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信