0

I am trying to use a BottomNavigationView with Android Navigation to create a standard tabbed application with three fragments: StatusFragment, MapFragment, and AlertsFragment.

I have my main_activity.xml with a FragmentContainerView and BottomNavigationView defined as follows:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.main.MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/mainNavHostFragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/bottomNavigationView"
        app:defaultNavHost="true"
        app:navGraph="@navigation/main_nav_graph" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNavigationView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:menu="@menu/main_menu" />

</androidx.constraintlayout.widget.ConstraintLayout>

My main_nav_graph.xml:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_nav_graph"
    app:startDestination="@id/mainActivity">
    <fragment
        android:id="@+id/statusFragment"
        android:name="<redacted>.ui.status.StatusFragment"
        android:label="StatusFragment" />
    <fragment
        android:id="@+id/mapFragment"
        android:name="<redacted>.ui.map.MapFragment"
        android:label="MapFragment" />
    <fragment
        android:id="@+id/alertsFragment"
        android:name="<redacted>.ui.alert.AlertsFragment"
        android:label="AlertsFragment" />
    <activity
        android:id="@+id/mainActivity"
        android:name="<redacted>.ui.main.MainActivity"
        android:label="main_activity"
        tools:layout="@layout/main_activity" />
</navigation>

And the associated main_menu.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/statusFragment"
        android:icon="@drawable/ic_status_24"
        android:title="@string/title_status" />
    <item
        android:id="@+id/mapFragment"
        android:icon="@drawable/ic_map_24"
        android:title="@string/title_map" />
    <item
        android:id="@+id/alertsFragment"
        android:icon="@drawable/ic_alert_24"
        android:title="@string/title_alerts" />

</menu>

In the onCreate method of my MainActivity, I configure navigation as follows:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        binding = MainActivityBinding.inflate(layoutInflater)
        val navHostFragment = supportFragmentManager.findFragmentById(R.id.mainNavHostFragment) as NavHostFragment
        val navController = navHostFragment.navController

        val appBarConfiguration = AppBarConfiguration(
            setOf(
                R.id.statusFragment,
                R.id.mapFragment,
                R.id.alertsFragment
            )
        )

        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration)
        NavigationUI.setupWithNavController(binding.bottomNavigationView, navController)
    }

The tab bar is displayed as expected, but tapping on the buttons does not do anything. I've made sure that the ids defined in main_menu.xml match those in main_nav_graph.xml. For being such a basic component of navigation on mobile, getting this to work on Android is proving to be frustratingly difficult; any help would be greatly appreciated.

beigirad
  • 4,986
  • 2
  • 29
  • 52
blau
  • 1,365
  • 15
  • 18
  • I suggest you to get rid of JetPack Navigation completely for your bottom nav implementation and configure fragment switching manually or use NavGraph library. Because using bottom nav with jetpack navigation forces fragment recreation everytime you switch tabs – Rinat Diushenov Feb 20 '21 at 13:04
  • Sorry to say it so plain, but everything @RinatDiushenov said is wrong. Use Jetpack, it works and is perfectly integrated with the lifecycle management of Android. Do not create some buggy custom implementations if you get the right tools offered by Google. – muetzenflo Feb 21 '21 at 13:14
  • 1
    @muetzenflo You are right :) But i wanna mention that did not say not to use jetpack navigation at all, but to only avoid it for bottomNavView. Do you think completely recreating fragments every time tab is switched is ok(potentially making expensive work over and over)? Because jetpack does that! there are workarounds though, that require loads of extra code. I'm aware that i'm not qualified to make these kind of radical suggestions, but it was based on things people well known in android world said. can name Zuinden) – Rinat Diushenov Feb 21 '21 at 14:21
  • @RinatDiushenov Interesting, thanks for clarification! I actually never run into a problem with the fragment being recreated (neither performance nor state saving). A quick "clean" workaround may be to sync a ViewPager with the BottomNav. Or to save the fragment state via `saveInstance` or using an `activitiyViewModel`. Do you konw if there is a technical reason behind this behavior? Or was Google just "lazy" so far? – muetzenflo Feb 21 '21 at 18:54
  • 1
    @muetzenflo as far as i know, the reason is that, under the hood, nav component uses replace() for switching tabs. So probably they are being lazy :D – Rinat Diushenov Feb 22 '21 at 04:52
  • There is [a similar case](https://stackoverflow.com/questions/76389519/navcontroller-navigater-id-tab-dashboard-messes-up-with-bottom-taps-normal-cl/76419028#76419028) when hitting a `BottomNavigationView` tab it doesn't do a selection – Zain Jun 07 '23 at 20:56

4 Answers4

9

Regarding the <fragment> Tag: Unfortunately, both current answers are wrong and it is actually discouraged by Google! Stop using the <fragment> tag. It is deprecated and will not be maintained anymore. You are correct to switch to <FragmentContainerView> which on top contains some lifecycle fixes that are not available in <fragment>

Your code looks almost perfect and I am pretty sure that it is not the navigation components, but the way you inflate the binding.

That said, here is a working code sample from one of my apps. Take special care that you:

  • create the Binding using the DataBindingUtil

  • set the desired fragment as startDestination this one was correctly spotted by @sercan!

  • give you <navigation> element an id (not mandatory, but to be safe)

  • use exactly the same imports! androidx.* everywhere, but material.bottomnavigation.BottomNavigtaionView.

  • the id of the menu item must match exactly the id of the fragment in the navi graph (you already mentioned that in your question. But I still add it here for completness.)

  • the FragmentContainerView really is the defaultNavHost for the given Activity.

activity_main.xml

<androidx.fragment.app.FragmentContainerView
    android:id="@+id/activity_repository_nav_host"
    android:name="androidx.navigation.fragment.NavHostFragment"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_graph_repository" />

<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/activity_repository_bottom_nav"
    app:menu="@menu/menu_repo_bottom" />

ActivityRepository

import androidx.databinding.DataBindingUtil
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView

private lateinit var navView: BottomNavigationView

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    DataBindingUtil.setContentView<ActivityRepositoryBinding>(this, R.layout.activity_repository).also { binding ->
        binding.lifecycleOwner = this
        binding.repositoryViewModel = repositoryViewModel
        binding.webHookViewModel = webHookViewModel
    }

    setupNavController()
}

private fun setupNavController() {
    val navHostFragment = supportFragmentManager.findFragmentById(R.id.activity_repository_nav_host) as NavHostFragment
    val navController = navHostFragment.navController
    
    navView = findViewById(R.id.activity_repository_bottom_nav)
    NavigationUI.setupWithNavController(navView, navController)
}

nav_graph_repository.xml

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/navigation_graph"
app:startDestination="@+id/fragment_downloads">

    <fragment
    android:id="@+id/fragment_downloads"
    android:name="net.onefivefour.android.bitpot.screens.downloads.DownloadsFragment"
    android:label="@string/downloads"
    tools:layout="@layout/fragment_downloads" />

</navigation>

menu_repo_bottom.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

   ...
   <item
    android:id="@+id/fragment_downloads"
    android:icon="@drawable/ic_downloads"
    android:title="@string/downloads" />

</menu>
Ryan M
  • 18,333
  • 31
  • 67
  • 74
muetzenflo
  • 5,653
  • 4
  • 41
  • 82
  • 2
    Thanks for the detailed response! Exactly as you said, it had nothing to do with my Navigation configuration but rather with how I was setting up my view binding... specifically, I was calling `setContentView(R.layout.main_activity)` instead of `setContentView(binding.root)`. A stupid mistake that cost me far more time than I'd care to admit, but glad to have that resolved. Cheers! – blau Feb 23 '21 at 21:08
  • You saved my hours of work by spotting the exact error which was with the way he (also me) was inflating the layout. – V Surya Kumar Apr 04 '22 at 10:05
  • this worked for me - the id of the menu item must match exactly the id of the fragment in the navi graph – Daan Dec 23 '22 at 17:03
2

Had the same error. What fixed it is using the same id for the fragments in the menu and navgraph xml files. menu:

<item
    android:id="@+id/navigation_home"
    android:icon="@drawable/ic_home"
    android:title="@string/home" />

<item
    android:id="@+id/navigation_explore"
    android:icon="@drawable/ic_explore"
    android:title="@string/explore" />

<item
    android:id="@+id/navigation_post"
    android:icon="@drawable/ic_post"
    android:title="@string/post" />

<item
    android:id="@+id/navigation_notifications"
    android:icon="@drawable/ic_notifications"
    android:title="@string/notifications" />

<item
    android:id="@+id/navigation_messages"
    android:icon="@drawable/ic_messages"
    android:title="@string/messages" />

navigation:

<fragment
    android:id="@+id/navigation_home"
    android:name="com.simpleapps22.dholuo.ui.home.HomeFragment"
    android:label="@string/home"
    tools:layout="@layout/fragment_home" />
<fragment
    android:id="@+id/navigation_explore"
    android:name="com.simpleapps22.dholuo.ui.explore.ExploreFragment"
    android:label="@string/explore"
    tools:layout="@layout/fragment_explore"/>
<fragment
    android:id="@+id/navigation_post"
    android:name="com.simpleapps22.dholuo.ui.post.PostFragment"
    android:label="@string/post"
    tools:layout="@layout/fragment_post"/>
<fragment
    android:id="@+id/navigation_notifications"
    android:name="com.simpleapps22.dholuo.ui.notifications.NotificationsFragment"
    android:label="@string/notifications"
    tools:layout="@layout/fragment_notifications" />
<fragment
    android:id="@+id/navigation_messages"
    android:name="com.simpleapps22.dholuo.ui.messages.MessagesFragment"
    android:label="@string/messages"
    tools:layout="@layout/fragment_messages" />
ofa
  • 21
  • 1
  • 5
0

If still relevant, here is the solution: https://github.com/Codeveyor/Android-Tab-Bar Fully customizable, flexible, and easy to maintain. Run test project on device or emulator to check if it matches your expectations

Olex
  • 1,656
  • 3
  • 20
  • 38
0

In My Case, I used ViewPager2 and connect it with BottomNavigationView using OnPageChangeCallback().

MainActivity.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity6">

    <androidx.viewpager2.widget.ViewPager2
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/viewpager"
        android:layout_above="@+id/bottom_navigation"/>

    <LinearLayout
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true">
        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/bottom"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:menu="@menu/bottom_menu"
            app:itemIconTint="@color/custom_bottom"
            app:itemTextColor="@color/custom_bottom"
            android:background="@color/black"/>
    </LinearLayout>


</RelativeLayout>

ViewPagerAdapter : (I Used FragmentStateAdapter(fragmentActivity FragmentActivity) Because FragmentPagerAdapter is depecrated)

class ViewPagerAdapter(fragmentActivity: FragmentActivity): FragmentStateAdapter(fragmentActivity){

    override fun getItemCount(): Int {
        return 3
    }

    override fun createFragment(position: Int): Fragment {
        return when(position){
            0 -> {
                HomeFragment()
            }
            1 -> {
                HomeFragment2()
            }
            2 -> {
                HomeFragment3()
            }
            else -> {
                throw Resources.NotFoundException("Position Not Found")
            }
        }
    }
}

And Now I will show u the answer of ur question Note I used binding just to replace findByViewId MainActivity.kt:

class MainActivity6 : AppCompatActivity() {

    private lateinit var binding: ActivityMain6Binding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMain6Binding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        setupBottom()
    }

    private fun setupBottom(){
        binding.viewpager.adapter = ViewPagerAdapter(this)
        binding.viewpager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback(){
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
                when(position){
                    0 -> {
                        binding.bottom.menu.findItem(R.id.page_1).isChecked = true
                    }
                    1 -> {
                        binding.bottom.menu.findItem(R.id.page_2).isChecked = true
                    }
                    2 -> {
                        binding.bottom.menu.findItem(R.id.page_3).isChecked = true
                    }
                }
            }
        })
        binding.bottom.setOnItemSelectedListener { item ->
            when(item.itemId){
                R.id.page_1 -> {
                    binding.viewpager.currentItem = 0
                    true
                }
                R.id.page_2 -> {
                    binding.viewpager.currentItem = 1
                    true
                }
                R.id.page_3 -> {
                    binding.viewpager.currentItem = 2
                    true
                }
                else -> false
            }
        }
    }

    // If the user is currently looking at the first step, allow the system to handle the
    // Back button. This calls finish() on this activity and pops the back stack.
    override fun onBackPressed() {
        if(binding.viewpager.currentItem == 0){
           super.onBackPressed()
        }
        else {
            // Otherwise, select the previous step.
            binding.viewpager.currentItem = binding.viewpager.currentItem - 1
        }
    }
}