16

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

talhaoz
  • 259
  • 3
  • 11

6 Answers6

7

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.

talhaoz
  • 259
  • 3
  • 11
  • 4
    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 Review](/review/late-answers/30810762) – Vapid Jan 18 '22 at 10:34
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.
camelcc
  • 222
  • 4
  • 9
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.

skyridertk
  • 103
  • 1
  • 8
  • Hi, I have am trying to work with this but can't seem to figure it out. https://stackoverflow.com/questions/70401623/navigating-to-a-bottom-tab-screen-from-a-single-screen-with-a-button-jetpack-com?noredirect=1#comment124448846_70401623 – King Dec 18 '21 at 21:13
  • @skyridertk 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. – lostintranslation Jun 09 '22 at 17:23
  • @lostintranslation 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 mixup – skyridertk Jun 11 '22 at 03:53
1

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

asprog_ii
  • 37
  • 2
  • 3
    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](/help/how-to-answer). – Community Dec 13 '21 at 10:46
  • Correcto. Basically you have to create a new separate `navHostConroller` for your bottom navigation. – lenooh Dec 27 '21 at 17:22
1

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()
}
Suryavel TR
  • 3,576
  • 1
  • 22
  • 25
0

use rememberNavController() for your function

fun YourFunction(
    navController: NavHostController = rememberNavController()
)
Mahdi Zareei
  • 1,299
  • 11
  • 18