Search code examples
androidandroid-navigationandroid-jetpack-navigationandroid-bottomnavandroid-splashscreen

Multiple versions of BottomNavigationView


I would like to have two versions of the BottomNavigationView, each with its own set of tabs. Some are shared, some not. For example, Version 1 has Fragment A, B, C, D on the bottom, version two has Fragment B, C, E, F on the bottom. I would like to inflate the view and the start destination based on the type of the user. So ideally my flow would be:

  1. fetch the user, or prompt log in
  2. depending on the user, inflate version 1 with start destination A, or version 2 with start destination F.
  3. whenever the version 1 pops up, it needs to pop up to destination A, whenever version 2 pops up it needs to pop up to destination F.

What is the best way to implement this navigation stack. There are obviously shared destinations, but there are also times when version 1 should never end up on fragments E, and F, and version 2 should never accidentally see fragments A and D.

I have tried the following code in main activity, but that works very clunky. A lot of times when the user navigates up, for a split second we show a wrong version to the user.

I'm using Kotlin with MVVM, so activity_main has the following:

<com.google.android.material.bottomnavigation.BottomNavigationView
                    android:id="@+id/bottom_navigation_view"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_alignParentBottom="true"
                    android:layout_gravity="bottom"
                    app:itemBackground="?attr/colorSurface"
                    app:labelVisibilityMode="labeled">

And in the Main activity I've added all of these clunks of code:

    override fun onNavigateUp(): Boolean {
        Timber.e("navigate up: ${navController?.currentDestination?.id}")
        Timber.e("navigate up: ${navController?.previousBackStackEntry?.id}")
        return super.onNavigateUp()
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Hide the default menu options to prevent duplication.
        return false
    }


    private fun inflateBottomNavigation(user: User) {
        if (isXUser(user)) {
            binding.bottomNavigationView.inflateMenu(R.menu.menu_bottom_nav_x)
            navController?.navigateSafely(
                R.id.home_fragment) // Set the top fragment to HomeFragment.
            navController?.graph?.setStartDestination(R.id.home_fragment)
        } else {
            binding.bottomNavigationView.inflateMenu(R.menu.menu_bottom_nav_y)
            navController?.navigateSafely(
                R.id.today_fragment) // Set the top fragment to TodayFragment.
            navController?.graph?.setStartDestination(R.id.today_fragment)
        }

        // Set up bottom navigation.
        binding.bottomNavigationView.setupWithNavController(navController!!)

        binding.bottomNavigationView.setOnItemSelectedListener { item ->
            when (item.itemId) {
                R.id.home_fragment,
                R.id.today_fragment -> {
                    if (isXUser(user)) {
                        navController?.navigateSafely(R.id.home_fragment)
                    } else {
                        navController?.navigateSafely(R.id.today_fragment)
                    }
                    true
                }
                R.id.chat_fragment,
                R.id.chat_x_fragment -> {
                    if (isXUser(user)) {
                        navController?.navigateSafely(R.id.chat_x_fragment)
                    } else {
                        navController?.navigateSafely(R.id.chat_fragment)
                    }
                    true
                }
                else -> {
                    navController?.navigateSafely(item.itemId)
                    true
                }
            }
        }
    }

    private fun isXUser(user: User): Boolean {
        return user.isDebugger == true
    }

    private fun setupNavigation() {
        navHostFragment =
            supportFragmentManager.findFragmentById(R.id.nav_host_fragment_container)
                as NavHostFragment
        navController = navHostFragment?.navController
        appBarConfiguration =
            AppBarConfiguration(
                setOf(
                    R.id.home_fragment,
                    R.id.today_fragment,
                    R.id.test_fragment,
                    R.id.chat_fragment,
                    R.id.chat_x_fragment,
                    R.id.progress_fragment,
                    R.id.points_fragment),
                binding.drawerLayout)

        binding.toolbar.setupWithNavController(navController!!, appBarConfiguration!!)
        binding.drawerNavView.setupWithNavController(navController!!)
      ...

      // Set up bottom navigation.
      binding.bottomNavigationView.setupWithNavController(navController!!)
    }

    private fun setUpUserObservers() {
        userViewModel.user.observe(
            this,
            Observer { user ->
        inflateBottomNavigation(user)
    }

This makes the experience very clunky, each time the user ends on the today/home fragment we re-inflate. A lot of times a wrong start destination can be shown to a user before the correct one shows. What is the best approach to structure the app, and handle switching between different types of users, without duplicating too much code? Thanks


Solution

  • I would suggest you to create a navGraph file in the navigation directory , if you don't know how to do that you can refer it here https://developer.android.com/guide/navigation/navigation-getting-started.

    And to implement the navGraph, in the acitivy_main.xml you would have to add FragmentContainerView above the BottomNavGraph.

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />
    

    Then in the main activity we would programmatically change the startDestination of the navGraph based on the userType. And I think creating two different menu files for different would be simpler.

    Here's the main activity code

    class MainActivity : AppCompatActivity() {
        private lateinit var navController: NavController
        private lateinit var bottomNavigationView: BottomNavigationView
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
            navController = navHostFragment.navController
            val navGraph = navController.navInflater.inflate(R.navigation.nav_graph)
    
            val userType = fetchUserType()
    
            val startDestination = if (userType == UserType.TYPE1) {
                R.id.fragmentA 
            } else {
                R.id.fragmentF 
            }
    
            navGraph.startDestination = startDestination
            navController.graph = navGraph
    
            bottomNavigationView = findViewById(R.id.bottom_navigation_view)
            if (userType == UserType.TYPE1) {
                bottomNavigationView.inflateMenu(R.menu.menu_version1)
            } else {
                bottomNavigationView.inflateMenu(R.menu.menu_version2)
            }
    
            bottomNavigationView.setupWithNavController(navController)
        }