Search code examples
androidbottomnavigationviewandroid-bottomnavigationview

navController.navigate(R.id.tab_dashboard) messes up with bottom taps. Normal click on bottom tabs stop working


Facing an issue where using navController.navigate(R.id.tab_dashboard) to dynamically switch to a bottom navigation tab messes up with bottom navigation.

Attached a video to explain the issue. Video link: https://github.com/hishamMuneer/BottomNavSample/blob/master/video/bottomnav_issue.mp4 Sample code here: https://github.com/hishamMuneer/BottomNavSample.git


class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

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

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val navView: BottomNavigationView = binding.navView

        val navController = findNavController(R.id.nav_host_fragment_activity_main)
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        val appBarConfiguration = AppBarConfiguration(
            setOf(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications
            )
        )
        setupActionBarWithNavController(navController, appBarConfiguration)
        navView.setupWithNavController(navController)

        val fab = binding.fab
        fab.setOnClickListener {
            navController.navigate(R.id.navigation_dashboard)
        }
    }
}

Solution

  • I want to understand why calling navController.navigate(R.id.tab_dashboard) is messing up with the normal tap on the on tabs.

    Disclaimer: This answer targets the demonstrations why this behavior happens per OP request. @Arda Kazancı thankfully already provides the answer.


    Navigating to some BottonNavigationView fragment using a NavController action doesn't actually achieve a real BottonNavigationView menu item selection; instead it does a fragment transaction on the fragment placeholder, i.e., the NavHostFragment.

    That is because the action has just abstract information to the destination fragment whether the destination fragment is a part of BottomNavigationView, or just a normal fragment.

    So, when using the action navController.navigate(R.id.action_global_dashBoardFragment) will not make the navView.setupWithNavController(navController) do its job, or at least will distub it.

    In your shared video, after hitting the fab, and then trying to hit the unsuccessful selection Home tab, that doesn't do a true selection because the NavigationBarView.OnItemSelectedListener callback returns false (keeping the current Dashboard tab selected instead of the Home fragment).

    This can be demonstrated by using a wrapper around the NavigationBarView.OnItemSelectedListener interface to log the returned value of the onNavigationItemSelected() callback:

    Use the below custom MyBottomNavigationView in the activity_main.xml layout instead of the <com.google.android.material.bottomnavigation.BottomNavigationView:

    class MyBottomNavigationView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null
    ) : BottomNavigationView(context, attrs) {
    
        override fun setOnItemSelectedListener(listener: OnItemSelectedListener?) {
            // wrapper to log the returned value of the default listener 
            super.setOnItemSelectedListener(OnItemSelectedListenerWrapper(listener))
        }
    
    }
    

    The wrapper interface:

    import com.google.android.material.navigation.NavigationBarView
    
    class OnItemSelectedListenerWrapper(private val listener: NavigationBarView.OnItemSelectedListener?) :
        NavigationBarView.OnItemSelectedListener {
        override fun onNavigationItemSelected(item: MenuItem): Boolean {
            val returnedValue = listener!!.onNavigationItemSelected(item)
            Log.d("LOG_TAG", "onNavigationItemSelected: $returnedValue")
            return returnedValue
        }
    }
    

    Now catching that log by doing the exact scenario shared in your video:

     D  onNavigationItemSelected: true
     D  onNavigationItemSelected: true
     D  onNavigationItemSelected: true
     D  onNavigationItemSelected: true
     D  onNavigationItemSelected: true
     D  onNavigationItemSelected: true
     D  onNavigationItemSelected: true
     D  onNavigationItemSelected: true
     D  onNavigationItemSelected: true
     D  onNavigationItemSelected: true
     D  onNavigationItemSelected: false  // <<<<< Here the Home tab is hit, but it keeps on the Dashboard
    

    The callback returns true (i.e., it's a successful tab selection) until hitting the fab just before the last log which returns false (which is not considered a tab selection, so keep the current tab selected) which occurs when the Home tab is hit, but the Dashboard keeps on because the fab action (R.id.action_global_dashBoardFragment) already disturbed the functionality of the navView.setupWithNavController(navController).

    The answer is to never use actions with BottomNavigationView fragments which are automatically managed by the NavController with setupWithNavController(), but to imitate using the tab pragmatically as demonstrated by @Arda Kazancı in order to make the automation of the setupWithNavController() apply:

    binding.navView.selectedItemId = R.id.navigation_dashboard