0

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

Ana
  • 67
  • 5

1 Answers1

1

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)
    }
zaid khan
  • 825
  • 3
  • 11
  • Thank you this is great! I was thinking into this direction, but the code becomes clunky because `fetchUserType` is an asynchronous call. There also needs to be a login flow if the user opens the app for the first time, before we know what type the user is. I guess I could just call this part of the code each time I detect a change. – Ana May 23 '23 at 11:33
  • @Ana can't you do the asynchronous call of fetching the user on the login/register page itself and then pass the result to main page – zaid khan May 23 '23 at 12:04