Issue
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:
- How can I dynamically add a fifth item based on some conditions OR
- 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 first solution, it is item4 -> fragment1 -> fragment2 -> acitivity recreated -> fragment (state recreated)
Answered By - Andrew
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.