JetpackCompose 导航嵌套图导致 "ViewModelStore should be set before setGraph call" 异常

JetpackCompose Navigation Nested Graphs cause "ViewModelStore should be set before setGraph call" exception

我正在尝试将 Jetpack Compose 导航应用到我的应用程序中。

我的屏幕:Login/Register 个屏幕和底部导航栏屏幕(通话、聊天、设置)。

我已经发现最好的方法是使用嵌套图。

但我一直收到 ViewModelStore should be set before setGraph call 异常。但是,我认为这不是正确的例外。

我的导航已经是最新版本了。可能是我的嵌套图逻辑不对。

要求: 我希望能够从登录或注册屏幕导航到任何 BottomBar 屏幕并反向

@Composable
fun SetupNavGraph(
    navController: NavHostController,
    userViewModel: UserViewModel
) {
    NavHost(
        navController = navController,
        startDestination = BOTTOM_BAR_GRAPH_ROUTE,
        route = ROOT_GRAPH_ROUTE
    ) {
        loginNavGraph(navController = navController, userViewModel)
        bottomBarNavGraph(navController = navController, userViewModel)
    }
}

NavGraph.kt

fun NavGraphBuilder.loginNavGraph(
    navController: NavHostController,
    userViewModel: UserViewModel
) {
    navigation(
        startDestination = Screen.LoginScreen.route,
        route = LOGIN_GRAPH_ROUTE
    ) {
        composable(
            route = Screen.LoginScreen.route,
            content = {
                LoginScreen(
                    navController = navController,
                    loginViewModel = userViewModel
                )
            })
        composable(
            route = Screen.RegisterScreen.route,
            content = {
                RegisterScreen(
                    navController = navController,
                    loginViewModel = userViewModel
                )
            })
    }
}

登录NavGraph.kt

fun NavGraphBuilder.bottomBarNavGraph(
    navController: NavHostController,
    userViewModel: UserViewModel
) {
    navigation(
        startDestination = Screen.AppScaffold.route,
        route = BOTTOM_BAR_GRAPH_ROUTE
    ) {
        composable(
            route = Screen.AppScaffold.route,
            content = {
                AppScaffold(
                    navController = navController,
                    userViewModel = userViewModel
                )
            })
    }
}

底部栏NavGraph.kt

@Composable
fun AppScaffold(
    navController: NavHostController,
    userViewModel: UserViewModel
) {
    val scaffoldState = rememberScaffoldState()

    Scaffold(

        bottomBar = {
            BottomBar(mainNavController = navController)
        },
        scaffoldState = scaffoldState,

        ) {

        NavHost(
            navController = navController,
            startDestination = NavigationScreen.EmergencyCallScreen.route
        ) {
            composable(NavigationScreen.EmergencyCallScreen.route) {
                EmergencyCallScreen(
                    navController = navController,
                    loginViewModel = userViewModel
                )
            }
            composable(NavigationScreen.ChatScreen.route) { ChatScreen() }
            composable(NavigationScreen.SettingsScreen.route) {
                SettingsScreen(
                    navController = navController,
                    loginViewModel = userViewModel
                )
            }
        }
    }
}

AppScaffold.kt

@Composable
fun BottomBar(mainNavController: NavHostController) {

    val items = listOf(
        NavigationScreen.EmergencyCallScreen,
        NavigationScreen.ChatScreen,
        NavigationScreen.SettingsScreen,
    )

    BottomNavigation(
        elevation = 5.dp,
    ) {
        val navBackStackEntry by mainNavController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route
        items.map {
            BottomNavigationItem(
                icon = {
                    Icon(
                        painter = painterResource(id = it.icon),
                        contentDescription = it.title
                    )
                },
                label = {
                    Text(
                        text = it.title
                    )
                },
                selected = currentRoute == it.route,
                selectedContentColor = Color.White,
                unselectedContentColor = Color.White.copy(alpha = 0.4f),
                onClick = {
                    mainNavController.navigate(it.route) {
                        mainNavController.graph.startDestinationRoute?.let { route ->
                            popUpTo(route) {
                                saveState = true
                            }
                        }
                        restoreState = true
                        launchSingleTop = true
                    }
                },

                )
        }

    }
}

BottomBar.kt

const val ROOT_GRAPH_ROUTE = "root"
const val LOGIN_GRAPH_ROUTE = "login_register"
const val BOTTOM_BAR_GRAPH_ROUTE = "bottom_bar"

sealed class Screen(val route: String) {
    object LoginScreen : Screen("login_screen")
    object RegisterScreen : Screen("register_screen")
    object AppScaffold : Screen("app_scaffold")

}

Screen.kt

sealed class NavigationScreen(val route: String, val title: String, @DrawableRes val icon: Int) {
    object EmergencyCallScreen : NavigationScreen(
        route = "emergency_call_screen",
        title = "Emergency Call",
        icon = R.drawable.ic_phone
    )

    object ChatScreen :
        NavigationScreen(
            route = "chat_screen",
            title = "Chat",
            icon = R.drawable.ic_chat)

    object SettingsScreen : NavigationScreen(
        route = "settings_screen",
        title = "Settings",
        icon = R.drawable.ic_settings
    )
}

导航Screen.kt

不允许嵌套 NavHost。它导致 ViewModelStore 应该在 setGraph 调用异常之前设置。通常,底部导航位于 NavHost 之外,这就是 docs 显示的内容。推荐的方法是单个 NavHost,您可以在其中根据您所在的目的地隐藏和显示底部导航。

一个 NavHost,一个 NavHostController。在 AppScaffold 上嵌套的 NavHost 前面创建一个新的 NavHostController。

在为这个问题苦苦挣扎了一段时间之后,我通过使用两个独立的 NavHost 解决了问题。这可能不是正确的方法,但目前有效。您可以在此处找到示例源代码:

https://github.com/talhaoz/JetPackCompose-LoginAndBottomBar

希望他们在即将发布的版本中使导航更容易。

在实现这个常见的 UI 模式时遇到类似问题:

  1. 主页(带有 BottomNavigationBar),此页面由 Inner nav controller
  2. 托管
  3. 单击一页的某些链接
  4. 导航到新页面(使用新的脚手架实例)。此页面由 Outer nav controller.
  5. 托管

通过使用 2 个 NavHost 和 2 个 navController 实例,Kinda 破解了 这个问题。

基本想法是使用一些消息通道来告诉 outer nav controller,在我的例子中是 Channel

private val _pages: Channel<String> = Channel()
var pages = _pages.receiveAsFlow()

@Composable
fun Route() {
    val navController1 = rememberNavController()
    LaunchedEffect(true) {
        pages.collect { page ->
            navController1.navigate("detail")
        }
    }


    NavHost(navController = navController1, startDestination = "home") {
        composable("home") { MainPage() }
        composable("detail") { DetailPage() }
    }
}

@Composable
fun MainPage() {
    val navController2 = rememberNavController()

    val onTabSelected = { tab: String ->
        navController2.navigate(tab) {
            popUpTo(navController2.graph.findStartDestination().id) { saveState = true }
            launchSingleTop = true
            restoreState = true
        }
    }
    Scaffold(topBar = { TopAppBar(title = { Text("Home Title") }) },
        bottomBar = {
            BottomNavigation {
                val navBackStackEntry by navController2.currentBackStackEntryAsState()
                val currentDestination = navBackStackEntry?.destination
                BottomNavigationItem(
                    selected = currentDestination?.hierarchy?.any { it.route == "tab1" } == true,
                    onClick = { onTabSelected("tab1") },
                    icon = { Icon(imageVector = Icons.Default.Favorite, "") },
                    label = { Text("tab1") }
                )
                BottomNavigationItem(
                    selected = currentDestination?.hierarchy?.any { it.route == "tab2" } == true,
                    onClick = { onTabSelected("tab2") },
                    icon = { Icon(imageVector = Icons.Default.Favorite, "") },
                    label = { Text("tab2") }
                )
                BottomNavigationItem(
                    selected = currentDestination?.hierarchy?.any { it.route == "tab3" } == true,
                    onClick = { onTabSelected("tab3") },
                    icon = { Icon(imageVector = Icons.Default.Favorite, "") },
                    label = { Text("tab3") }
                )
            }
        }
    ) { value ->
        NavHost(navController = navController2, startDestination = "tab1") {
            composable("tab1") { Home() }
            composable("tab2") { Text("tab2") }
            composable("tab3") { Text("tab3") }
        }
    }
}

class HomeViewModel: ViewModel()
@Composable
fun Home(viewModel: HomeViewModel = HomeViewModel()) {
    Button(
        onClick = {
            viewModel.viewModelScope.launch {
                _pages.send("detail")
            }
        },
        modifier = Modifier.padding(all = 16.dp)
    ) {
        Text("Home", modifier = Modifier.padding(all = 16.dp))
    }
}

@Composable
fun DetailPage() {
    Scaffold(topBar = { TopAppBar(title = { Text("Detail Title") }) }) {
        Text("Detail")
    }
}

缺点:

  1. 应用需要维护UI栈信息。
  2. 应对响应式布局更加困难。