Search code examples
androidkotlinbottomnavigationviewandroid-architecture-navigationandroid-navigation

Error: Maximum number of items supported by BottomNavigationView is 5, after removing item and adding a new one


Currently, I have the requirement to dynamically change my bottomnavigation#menu when the user logs out or logs in. With this, the bottomnavigation#menu is either R.menu.user_logged_in or R.menu.user_logged_out. My first approach was like this:

private fun setUpBottomNav(newMenu: Int) {
    with(binding.bottomNavigationView) {
        menu.clear()
        inflateMenu(newMenu)
        setupWithNavController(findNavController(R.id.fragment_container))
        // fix blinking when re selecting bottom nav item
        setOnItemReselectedListener {}
    }
}

The major issue with this approach was, that when the activity gets recreated (for example when the process is killed or when you open a web browser from the app via Intent and then click the back button), the menu would be cleared and inflated again via menu#clear and inflateMenu, resulting in the lost of the current bottomnavigation state (e.g when the profile-tab was selected and menu#clear was called in activity#oncreate, the selected state would be lost and the home-tab would be selected).

My next Idea was to reduce the item_amount in my R.menu_user_logged_out to four items and instead add a fifth menu-item at runtime by checking if the user is logged in our logged out. This would be my second approach:

// NO CLUE WHAT Menu.NONE, ..., Menu.NONE means!!!!
private fun inflateFifthMenuItem() {
   if(user.isLoggedIn) {
       binding.bottomNavigationView.menu.add(Menu.NONE, R.id.userLoggedInFragment, Menu.NONE, "Profil").setIcon(R.drawable.child_selector_profil)
   } else {
       binding.bottomNavigationView.menu.add(Menu.NONE, R.id.userLoggedOutFragment, Menu.NONE, "Profil").setIcon(R.drawable.child_selector_profil)
   }
}

Now even tho the above solution sound logical, android has another opinion:

   java.lang.IllegalArgumentException: Maximum number of items supported by BottomNavigationView is 5. Limit can be checked with BottomNavigationView#getMaxItemCount()
        at com.google.android.material.navigation.NavigationBarMenu.addInternal(NavigationBarMenu.java:67)
        at androidx.appcompat.view.menu.MenuBuilder.add(MenuBuilder.java:476)
        at com.example.app.presentation.main.MainActivity.signInUserBottomNav(MainActivity.kt:94)
        at com.example.app.presentation.main.MainActivity.observeLoginState$lambda-2(MainActivity.kt:75)
        at com.example.app.presentation.main.MainActivity.$r8$lambda$-vuA_npkMdEgJfGZeCrh_HfU3LQ(Unknown Source:0)
        at com.example.app.presentation.main.MainActivity$$ExternalSyntheticLambda0.onChanged(Unknown Source:4)
        at androidx.lifecycle.LiveData.considerNotify(LiveData.java:133)
        at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:151)
        at androidx.lifecycle.LiveData.setValue(LiveData.java:309)
        at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
        at androidx.lifecycle.LiveDataScopeImpl$emit$2.invokeSuspend(CoroutineLiveData.kt:99)
        at androidx.lifecycle.LiveDataScopeImpl$emit$2.invoke(Unknown Source:10)
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:165)
        at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
        at androidx.lifecycle.LiveDataScopeImpl.emit(CoroutineLiveData.kt:97)
        at com.example.app.presentation.main.ActivityViewModel$loginState$1.invokeSuspend(ActivityViewModel.kt:26)
        at com.example.app.presentation.main.ActivityViewModel$loginState$1.invoke(Unknown Source:8)
        at com.example.app.presentation.main.ActivityViewModel$loginState$1.invoke(Unknown Source:4)
        at androidx.lifecycle.BlockRunner$maybeRun$1.invokeSuspend(CoroutineLiveData.kt:176)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:367)
        at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.startCoroutineImpl(Builders.common.kt:192)
        at kotlinx.coroutines.BuildersKt.startCoroutineImpl(Unknown Source:1)
        at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:134)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56)
        at kotlinx.coroutines.BuildersKt.launch(Unknown Source:1)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47)
        at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source:1)
        at androidx.lifecycle.BlockRunner.maybeRun(CoroutineLiveData.kt:174)
        at androidx.lifecycle.CoroutineLiveData.onActive(CoroutineLiveData.kt:240)
        at androidx.lifecycle.LiveData.changeActiveCounter(LiveData.java:390)
        at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged(LiveData.java:466)
        at androidx.lifecycle.LiveData$LifecycleBoundObserver.onStateChanged(LiveData.java:425)
        at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
        at androidx.lifecycle.LifecycleRegistry.forwardPass(LifecycleRegistry.java:265)
        at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:307)
        at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:148)
        at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:134)
        at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:68)
        at androidx.lifecycle.ReportFragment$LifecycleCallbacks.onActivityPostStarted(ReportFragment.java:187)
        at android.app.Activity.dispatchActivityPostStarted(Activity.java:1248)
        at android.app.Activity.performStart(Activity.java:7865)
        at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3294)
        at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221)
        at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

So my question is:

  1. How can I dynamically add a fifth item based on some conditions OR
  2. How can I dynamically change the menu based on some condition (e.g when the user logs in, pressing on the profil button should navigate you to the userLoggedInFragment and not userLoggedOutFragment

Solution

  • Okay, I've managed to solve my problem and achieved to set a menu / item at runtime without losing its current state. There are two possible solutions. Solution 1 is setting a different menu_item and solution two is changing one specific id. Important: For Solution 2, your menu should only contain x - 1 items at the xml.

    For example, if you want to have 5 items in your bottom nav, your xml should only contain 4 items. The fifth item will be set at runtime

    Solution one

     private fun setUpBottomProfile(itemId: Int) {
        val controller = (supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment).navController
    
        with(binding.bottomNavigationView) {
            val currentSelectedItem = selectedItemId
    
            menu.clear()
            inflateMenu(menuId)
            
            selectedItemId = currentSelectedItem
    
            setupWithNavController(controller)
            // fix blinking when re selecting bottom nav item
            setOnItemReselectedListener {}
    
        }
    }
    

    Solution two

    private fun setUpBottomProfile(itemId: Int) {
        val controller = (supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment).navController
    
        with(binding.bottomNavigationView) {
            val currentSelectedItem = selectedItemId
    
            try {
                val lastItem = menu[4]
                if (lastItem.itemId != itemId) {
                    menu.removeItem(lastItem.itemId)
                    addProfilItem(menu, itemId)
                    selectedItemId = currentSelectedItem
                }
            } catch (e: IndexOutOfBoundsException) {
                addProfilItem(menu, itemId)
            }
    
            setupWithNavController(controller)
            // fix blinking when re selecting bottom nav item
            setOnItemReselectedListener {}
        }
    }
    
    private fun addProfilItem(menu: Menu, itemId: Int) {
        menu.add(0, itemId, 0, getString(R.string.bottomnav_description_profil))
            .setIcon(R.drawable.child_selector_profil)
    }
    

    Important

    I would advise to choose the second option because of the following problem: When choosing the first option, the entire menu gets cleared and another menu get's reinflated. With this, not only the menu but furthermore the entire bottomnavigation state is cleared.

    So for example if you click on item4 -> fragment1 -> fragment2 -> acitivity recreated -> item4 (state lost, not started from fragment2

    With the second solution, it is item4 -> fragment1 -> fragment2 -> acitivity recreated -> fragment2 (state recreated)