JetpackCompose 导航嵌套图导致 "ViewModelStore should be set before setGraph call" 异常
JetpackCompose Navigation Nested Graphs cause "ViewModelStore should be set before setGraph call" exception
-
kotlin
-
android-jetpack
-
android-architecture-navigation
-
android-jetpack-compose
-
jetpack-compose-navigation
我正在尝试将 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 模式时遇到类似问题:
- 主页(带有 BottomNavigationBar),此页面由
Inner nav controller
托管
- 单击一页的某些链接
- 导航到新页面(使用新的脚手架实例)。此页面由
Outer nav controller
. 托管
通过使用 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")
}
}
缺点:
- 应用需要维护UI栈信息。
- 应对响应式布局更加困难。
kotlin
android-jetpack
android-architecture-navigation
android-jetpack-compose
jetpack-compose-navigation
我正在尝试将 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 模式时遇到类似问题:
- 主页(带有 BottomNavigationBar),此页面由
Inner nav controller
托管
- 单击一页的某些链接
- 导航到新页面(使用新的脚手架实例)。此页面由
Outer nav controller
. 托管
通过使用 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")
}
}
缺点:
- 应用需要维护UI栈信息。
- 应对响应式布局更加困难。