EverDream
EverDream

Reputation: 51

Problem with Jetpack Compose Navigation in BottomNavigationBar

Based on the following image, my App currently contains 2 main routes : Auth and Dashboard. When the user started the App, an observer will check whether the user is logged in and direct he/she to the Login Page/Dashboard.

Navigation Flow

From the image as you can see, I've set the NavGraph(which include all the Dashboard routes) in the MainActivity. The problem is when user click one of the bottom navigation items, he/she will be directed straight to the view page. Instead, what I wanted is to show the user the view inside the scaffold content of the Dashboard. Check the image below.

Problem Encountered

Based on the info, how can I get the composable view inside the scaffold content once the user click the item?

Any guidance will be much appreciated. Thanks!

Side Note: This is my first compose project and I am trying to implement the Single Activity architecture without any fragment used. I apologize prior if my code violate any design or programming convention as I am quite new into programming[self-learner aka Hobbyist here :( ].

MainActivity.kt

package com.example.firstcomposeproject

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navigation
import com.example.firstcomposeproject.authview.FirebaseUserViewModel
import com.example.firstcomposeproject.authview.LogIn
import com.example.firstcomposeproject.authview.SignUpFragment
import com.example.firstcomposeproject.dashboard.*

import com.example.firstcomposeproject.ui.theme.FirstComposeProjectTheme


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val userViewModel: FirebaseUserViewModel by viewModels()

        setContent {
            FirstComposeProjectTheme(
                darkTheme = false
            ) {
                val navController = rememberNavController()


                /*
                Observing FirebaseUser LiveData to check if user is logged in. If yes, direct user
                to dashboard, else direct user to authenticate screen.
                */
                userViewModel.userMutableLiveData.observe(this, {
                        firebaseUser ->
                    if(firebaseUser != null){
                        navController.popBackStack()
                        navController.navigate("dashboard"){
                            popUpTo("auth"){
                                inclusive = true
                            }
                        }

                        userViewModel.clearFormField()
                    }else {
                        navController.navigate("auth"){
                            popUpTo("dashboard"){
                                inclusive = true
                            }
                        }
                    }
                })


                //Navigation Graph For The Whole App
                NavHost(navController = navController, startDestination = "auth"){
                    navigation(startDestination = "logInUI", route = "auth"){
                        composable("logInUI"){
                            LogIn(
                                userViewModel,
                                onNavigate = {
                                    navigateTo(it, navController)
                                }
                            ).FinalView()
                        }

                        composable("signUpUI"){
                            SignUpFragment(
                                userViewModel,
                                onNavigate = {
                                    navigateTo(it, navController)
                                }
                            ).FinalView()
                        }
                    }
                    addDashBoardGraph(userViewModel, navController)
                }
            }
        }
    }

    private fun navigateTo(
        dest: String,
        navController: NavController
    ){
        navController.navigate(dest)
    }



}

Dashboard.kt

package com.example.firstcomposeproject.dashboard

import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navigation
import com.example.firstcomposeproject.authview.FirebaseUserViewModel
import com.example.firstcomposeproject.dashboard.helper.Screen

import com.example.firstcomposeproject.ui.theme.Teal200

fun NavGraphBuilder.addDashBoardGraph(
    userViewModel: FirebaseUserViewModel,
    navController: NavHostController
  ) {
    navigation(startDestination = "dashboardUI", route = "dashboard"){
        composable("dashboardUI"){
            Dashboard(
                navController,
                userViewModel
            )
        }

        composable("navigationUI"){
            Navigation()
        }

        composable("messageUI"){
            Message()
        }

        composable("historyUI"){
            History()
        }

        composable("profileUI"){
            Profile(
                userViewModel
            )
        }
    }
}


@Composable
fun Dashboard(
    navController: NavHostController,
    userViewModel: FirebaseUserViewModel
){
    val items = listOf(
        Screen.Navigation,
        Screen.Message,
        Screen.History,
        Screen.Profile
    )

    Scaffold(
        bottomBar = {
            BottomNavigation(
                backgroundColor = Color.White,
                contentColor = Teal200
            ) {
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentDestination = navBackStackEntry?.destination

                items.forEach{ screen ->
                    BottomNavigationItem(
                        label = { Text(text = screen.label)},
                        selected = currentDestination?.hierarchy?.any{ it.route == screen.route } == true,
                        onClick = {
                            navController.navigate(screen.route)
                            {

                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true

                                restoreState = true
                            }
                        },
                        icon = {
                            Icon(painterResource(id = screen.icon), screen.label)
                        }, unselectedContentColor = Color.Gray
                    )
                }

            }
        }
    ){
        //Scaffold Content - How to access the compose function from the MainActivity NavGraph here?
     }
}

Upvotes: 5

Views: 5066

Answers (2)

Arunabh Das
Arunabh Das

Reputation: 14342

You should do it as follows -

In your MainActivity, call MyBottomNavApp

    class MainActivity : ComponentActivity() {
    lateinit var navController: NavController
    private val viewModel: MyAppViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        WindowCompat.setDecorFitsSystemWindows(window, false)
        installSplashScreen().apply {
            setKeepOnScreenCondition {
                viewModel.isLoading.value
            }
        }
        setContent {
            UnicornTheme {
                // Uncomment below and MyApp lines to use drawer navigation
                navController = rememberNavController()
                // MyApp(navController)
                // Uncomment MyBottomNavApp to get bottomnav navigation
                MyBottomNavApp(navController as NavHostController)
            }
        }
    }

    override fun onBackPressed() {
        super.onBackPressed()
    }
}

Then, implement MyBottomNavApp with a NavHost as below

@Composable
fun MyBottomNavApp(
    navController: NavHostController
) {
    Scaffold(
        modifier = Modifier.navigationBarsPadding(),
        bottomBar = { MyBottomNavigation(navController = navController)},
    ) { innerPadding ->
        Box(Modifier.padding(innerPadding)) {
            NavHost(
                navController = navController,
                startDestination = Home.route
                ) {
                composable(Home.route) {
                    DashboardScreen(navController = navController)
                }

                composable(Settings.route) {
                    NavigationScreen(navController = navController)
                }
            }
        }
    }
}

Then, implement Destinations as below -

interface Destinations {
    val route: String
    val icon: ImageVector
    val title: String
}

object Home: Destinations {
    override val route = "Dashboard"
    override val icon = Icons.Filled.Dashboard
    override val title = "Dashboard"
}

object Settings: Destinations {
    override val route = "Navigation"
    override val icon = Icons.Filled.Navigation
    override val title = "Navigation"
}

Then, implement MyBottomNavigation as below -

@Composable
fun MyBottomNavigation(
    navController: NavController
) {
    val destinationList = listOf<Destinations> (
        Dashboard,
        Navigation
    )

    val selectedIndex = rememberSaveable {
        mutableStateOf(0)
    }

    BottomNavigation(
        backgroundColor = colorResource(id = R.color.primary_contrast),
        contentColor = colorResource(id = R.color.secondary_contrast)
    ) {
        destinationList.forEachIndexed{index, destination ->
            BottomNavigationItem(
                label = { Text(text = destination.title)},
                icon = { Icon(imageVector = destination.icon, contentDescription = destination.title)},
                selected = index == selectedIndex.value,
                onClick = {
                    selectedIndex.value = index
                    navController.navigate(destinationList[index].route) {
                        popUpTo(Home.route)
                        launchSingleTop = true
                    }
                }
            )
        }
    }
}

You can see an example of this in the feature/develop_starter_kit_bottom_navigation branch of the following repo -

https://github.com/arunabhdas/unicorn-commerce/tree/feature/develop_starter_kit_bottom_navigation

Upvotes: 0

Stefano Sansone
Stefano Sansone

Reputation: 2709

Looking at your code, I think you need to set a NavHost in your Scaffold content in the Dashboard function, to tell the system you want to switch composables inside the layout with the bottomBar you created.

I see you already understand about nested navigation, so if you just want to reuse the same NavHost you can try to define it in a specific composable function.

@Composable
fun MyNavGraph(
    navController: NavHostController = rememberNavController(),
    userViewModel: FirebaseUserViewModel
){
    NavHost(navController = navController, startDestination = "auth"){
        navigation(startDestination = "logInUI", route = "auth"){
            composable("logInUI"){
                LogIn(
                    userViewModel,
                    onNavigate = {
                        navigateTo(it, navController)
                    }
                ).FinalView()
            }

            composable("signUpUI"){
                SignUpFragment(
                    userViewModel,
                    onNavigate = {
                        navigateTo(it, navController)
                    }
                ).FinalView()
            }
        }
        addDashBoardGraph(userViewModel, navController)
    }
}


I've not tried it, so let me know if fit your situation. Hope it helps a little.

Upvotes: 1

Related Questions