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

8

20

I am trying to apply Jetpack Compose navigation into my application.

My Screens: Login/Register screens and Bottom navbar screens(call, chat, settings).

I already found out that the best way to do this is using nested graphs.

But I keep getting ViewModelStore should be set before setGraph call exception. However, I don't think this is the right exception.

My navigation is already in the latest version. Probably my nested graph logic is not right.

Requirement: I want to be able to navigate from the Login or Register screen to any BottomBar Screen & reverse

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

}

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

NavigationScreen.kt

Hessney answered 27/10, 2021 at 12:6 Comment(10)
Hey did you figure it out? I have exact same problem as yoursFortenberry
Nope, still don't have an answer @FortenberryHessney
Having the same issue :/Lillia
Each NavController must be associated with a single NavHost composable. To fix that issue you need to pass new instance of rememberNavController() . developer.android.com/jetpack/compose/navigation#create-navhostKeifer
Already tried, it didn't work @KunalSharmaHessney
Facing the same problemMacbeth
stuck with this as wellHusha
Same here. How is there no documentation anywhere on how to handle this. Disappointing .Bedsore
Same here. Please make some notify when you got the answer.Lack of documentation and what to do. Please.Bruno
Same here, create two NavHost and pass new instance of rememberNavController() works! ThanksJordain
H
8

After struggling some time with this issue, I made my way out by using two separated NavHost. It might not be the right way to do it but it works at the moment. You can find the example source code here:

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

Hope they make the navigation easier on upcoming releases.

Hessney answered 13/1, 2022 at 15:18 Comment(1)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From ReviewEhrman
I
4

In my case, I had to create nav controller (for bottom bar) with in home screen.

@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()
}
Ives answered 5/11, 2022 at 10:42 Comment(0)
R
2

One NavHost, one NavHostController. Create a new NavHostController in front of the nested NavHost on AppScaffold.

Ramakrishna answered 13/12, 2021 at 10:33 Comment(2)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Hitch
Correcto. Basically you have to create a new separate navHostConroller for your bottom navigation.Silverstein
T
2

Have similar issue when implement this common UI pattern:

  1. HomePage(with BottomNavigationBar), this page is hosted by Inner nav controller
  2. click some links of one page
  3. navigate to a new page (with new Scaffold instance). This page is hosted by Outer nav controller.

Kinda hacked this issue by using 2 NavHost with 2 navController instance.

Basic idea is using some msg channel to tell the outer nav controller, a Channel in my case.

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

Cons:

  1. App needs to maintain UI stack information.
  2. It's even harder to cope with responsive layout.
Trisyllable answered 21/1, 2022 at 7:35 Comment(0)
M
1

Nesting of NavHost is not allowed. It results in ViewModelStore should be set before setGraph call Exception. Generally, the bottom nav is outside of the NavHost, which is what the docs show. The recommended approach is a single NavHost, where you hide and show your bottom nav based on what destination you are on.

Macbeth answered 13/11, 2021 at 16:14 Comment(3)
Hi, I have am trying to work with this but can't seem to figure it out. #70402123Bedsore
@Macbeth while I agree that this seems to be the generally accepted solution. Doesn't mean its not without issue. Seems compose navigation is still very limited. Biggest issue with showing/hiding the bottom bar is when navigating between routes with/without the bottom bar looks horrible. To fix this you need to pull in an additional library to assist with animation of the bottom bar between routes. Can only hope compose devs are seeing the amount of questions that are the same as the users question.Archival
@Archival That's not entirely true. You can make transitions between routes behave the way you want them to. It's not the library's fault. I implemented bottom nav hiding and it works exceptionally well. You don't need a library. I think it's just a mixupMacbeth
T
1

use rememberNavController() for your function

fun YourFunction(
    navController: NavHostController = rememberNavController()
)
Tanta answered 30/6, 2022 at 7:45 Comment(0)
R
0

I create a graph within the one of the app's graph screen and get this error message "ViewModelStore should be set before setGraph call"

When I created a new navController, the error went away.

Relations answered 15/4 at 10:25 Comment(0)
R
0

I was facing this issue where I had a screen A trying to navigate to a screen B which has its own navigation. A -> B (B1, B2, B3) -> C. If I launch just B and navigate from B1 to C (through a callback) it would work but navigating from A to B would make the app crash but only sometimes. Looking at this comment I thought the issue was that the inner navhost was being recomposed but not the inner navcontroller which I had declared at the Activity level. So the fix for me was to reasign the inner navController before calling the inner NavHost.

Ranchman answered 5/8 at 8:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.