最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

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

programmeradmin1浏览0评论

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 */ }
        }
    }
}

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论