Ошибка: Максимальное количество элементов, поддерживаемых BottomNavigationView, равно 5, после удаления элемента и добавления нового

#android #kotlin #bottomnavigationview #android-architecture-navigation #android-navigation

Вопрос:

В настоящее время у меня есть требование динамически изменять свой bottomnavigation#menu , когда пользователь выходит из системы или входит в систему. При этом нижним меню навигации#является либо R.menu.user_logged_in или R.menu.user_logged_out . Мой первый подход был таким:

 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 {}
    }
}
 

Основная проблема с этим подходом заключалась в том, что когда активность получает воссозданы (например, когда процесс будет убит, или когда вы открываете веб-браузер из приложения через намерение и затем щелкните кнопки «Назад»), Меню будет очищен и снова надувают через menu#clear а inflateMenu , в результате чего потерял текущего bottomnavigation государства (например.при этом профиль-вкладка была выбрана и menu#clear был назван в activity#oncreate , некоторые государства будут потеряны и дома-вкладка будет выбрана).

Моя следующая идея состояла в том, чтобы уменьшить количество элементов в моем R.menu_user_logged_out до четырех пунктов и вместо этого добавить пятый пункт меню во время выполнения, проверив, вошел ли пользователь в систему или вышел из системы. Это был бы мой второй подход:

 // 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)
   }
}
 

Теперь, даже если вышеупомянутое решение звучит логично, у Android есть другое мнение:

    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)
 

Поэтому мой вопрос таков:

  1. Как я могу динамически добавить пятый элемент на основе некоторых условий или
  2. Как я могу динамически изменять меню в зависимости от некоторых условий (например, когда пользователь входит в систему, нажатие кнопки профиля должно привести вас к фрагменту userLoggedInFragment, а не к фрагменту userloggedout

Ответ №1:

В начале BottomNavigationView начинается с пустого меню по умолчанию. После этого установите меню на основе таких условий, как это

   when (condition) {
    one -> bottomNavigationView.inflateMenu(R.menu.menu_bottom_navigation_one)
    two -> bottomNavigationView.inflateMenu(R.menu.menu_bottom_navigation_two)
    three ->bottomNavigationView.inflateMenu(R.menu.menu_bottom_navigation_three)
    else -> bottomNavigationView.inflateMenu(R.menu.menu_bottom_navigation_defaults)
}
 

Комментарии:

1. Да, это сработало бы, но что, если я захочу изменить свое меню во время выполнения? Например, предположим, что приложение запускается и условие == один, тогда меню 1 будет раздуто. Теперь, во время выполнения, я установил условие на условие == два. С учетом этого меню 2 теперь должно быть раздуто. Как я могу этого достичь?

Ответ №2:

Хорошо, мне удалось решить свою проблему и установить меню / пункт во время выполнения без потери его текущего состояния. Есть два возможных решения. Решение 1 устанавливает другой элемент menu_item, а решение два изменяет один конкретный идентификатор. Важно: Для решения 2 ваше меню должно содержать только пункты x — 1 в xml.

Например, если вы хотите иметь 5 элементов в нижней навигации, ваш xml должен содержать только 4 элемента. Пятый элемент будет установлен во время выполнения

Решение первое

  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 {}

    }
}
 

Решение второе

 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)
}
 

Важный

Я бы посоветовал выбрать второй вариант из-за следующей проблемы: при выборе первого варианта все меню очищается, а другое меню заполняется заново. При этом очищается не только меню, но и все состояние нижней навигации.

Так, например, если вы нажмете на item4 -> fragment1 -> fragment2 -> acitivity recreated -> item4 (state lost, not started from fragment2

Со вторым решением это item4 -> fragment1 -> fragment2 -> acitivity recreated -> fragment2 (state recreated)