JetpackCompose 导航嵌套图导致“ViewModelStore 应在 setGraph 调用之前设置”异常

问题描述 投票:0回答:7

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

我的屏幕:登录/注册屏幕和底部导航栏屏幕(通话、聊天、设置)。

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

但我不断遇到

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

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

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

@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
                )
            })
    }
}

LoginNavGraph.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
                )
            })
    }
}

BottomBarNavGraph.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")

}

屏幕.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
    )
}

导航屏幕.kt

kotlin android-jetpack-compose android-jetpack android-architecture-navigation jetpack-compose-navigation
7个回答
8
投票

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

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

希望他们能让即将发布的版本的导航变得更容易。


4
投票

就我而言,我必须在主屏幕中创建导航控制器(用于底部栏)。

@AndroidEntryPoint
class MainActivity: ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        setContent {
            Theme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    AppContainer()
                }
            }
        }
    }
}

@Composable
fun AppContainer() {
    val mainNavController = rememberNavController()
    // This was causing the issue. I moved this to HomeScreen.
    // val bottomNavController = rememberNavController()

    Box(
        modifier = Modifier.background(BackgroundColor)
    ) {
        NavGraph(mainNavController)
    }
}

@Composable
fun HomeScreen(mainNavController: NavController) {
     val bottomBarNavController = rememberNavController()
}

2
投票

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


2
投票

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

  1. 首页(带底部导航栏),此页面由
    Inner nav controller
  2. 托管
  3. 点击某一页的某些链接
  4. 导航到新页面(带有新的 Scaffold 实例)。此页面由
    Outer nav controller
    主持。

通过使用 2 个 NavHost 和 2 个 navController 实例来解决这个问题。 基本想法是使用一些消息通道来告诉

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")
    }
}

缺点:

App需要维护UI堆栈信息。
  1. 应对响应式布局就更难了。

1
投票
docs

显示的内容。推荐的方法是单个导航主机,您可以根据您所在的目的地隐藏和显示底部导航。


1
投票

fun YourFunction( navController: NavHostController = rememberNavController() )



0
投票

我应该创建一个新的 navController 并且错误已经消失。

© www.soinside.com 2019 - 2024. All rights reserved.