Reputation: 4743
Android Jetpack Navigation, BottomNavigationView with auto fragment back stack on back button click?
What I wanted, after choosing multiple tabs one after another by user and user click on back button app must redirect to the last page he/she opened.
I achieved the same using Android ViewPager, by saving the currently selected item in an ArrayList. Is there any auto back stack after Android Jetpack Navigation Release? I want to achieve it using navigation graph
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".main.MainActivity">
<fragment
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<android.support.design.widget.BottomNavigationView
android:id="@+id/navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/navigation" />
</android.support.constraint.ConstraintLayout>
navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@drawable/ic_home"
android:title="@string/title_home" />
<item
android:id="@+id/navigation_people"
android:icon="@drawable/ic_group"
android:title="@string/title_people" />
<item
android:id="@+id/navigation_organization"
android:icon="@drawable/ic_organization"
android:title="@string/title_organization" />
<item
android:id="@+id/navigation_business"
android:icon="@drawable/ic_business"
android:title="@string/title_business" />
<item
android:id="@+id/navigation_tasks"
android:icon="@drawable/ic_dashboard"
android:title="@string/title_tasks" />
</menu>
also added
bottomNavigation.setupWithNavController(Navigation.findNavController(this, R.id.my_nav_host_fragment))
I got one answer from Levi Moreira
, as follows
navigation.setOnNavigationItemSelectedListener {item ->
onNavDestinationSelected(item, Navigation.findNavController(this, R.id.my_nav_host_fragment))
}
But by doing this only happening is that last opened fragment's instance creating again.
Providing proper Back Navigation for BottomNavigationView
Upvotes: 81
Views: 67879
Reputation: 4743
Since Navigation version 2.4.0, BottomNavigationView with NavHostFragment supports a separate back stack for each tab, so Elyeante answer is 50% correct. But it doesn't support back stack for primary tabs. For example, if we have 4 main fragments (tabs) A, B, C, and D, the startDestination
is A. D has child fragments D1, D2, and D3. If user navigates like A -> B -> C ->D -> D1 -> D2-> D3, if the user clicks the back button with the official library the navigation will be D3 -> D2-> D1-> D followed by A. That means primary tabs B and C will not be in the back stack.
To support the primary tab back stack, I created a stack with a primary tab navigation reference. On the user's back click, I updated the selected item of BottomNavigationView based on the stack created.
I have created this Github repo to show what I did. I reached this answer with the following medium articles.
Steps to implement
Add the latest navigation library to Gradle and follow Official repo for supporting back stack for child fragments.
Instead of creating single nav_graph, we have to create separate navigation graphs for each bottom bar item and this three graph should add to one main graph as follows
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@+id/home">
<include app:graph="@navigation/home"/>
<include app:graph="@navigation/list"/>
<include app:graph="@navigation/form"/>
</navigation>
And link bottom navigation view and nav host fragment with setupWithNavController
Now the app will support back stack for child fragments. For supporting the main back navigation, we need to add more lines.
private var addToBackStack: Boolean = true
private lateinit var fragmentBackStack: Stack<Int>
The fragmentBackStack will help us to save all the visited destinations in the stack & addToBackStack is a checker which will help to determine if we want to add the current destination into the stack or not.
navHostFragment.findNavController().addOnDestinationChangedListener { _, destination, _ ->
val bottomBarId = findBottomBarIdFromFragment(destination.id)
if (!::fragmentBackStack.isInitialized){
fragmentBackStack = Stack()
}
if (needToAddToBackStack && bottomBarId!=null) {
if (!fragmentBackStack.contains(bottomBarId)) {
fragmentBackStack.add(bottomBarId)
} else if (fragmentBackStack.contains(bottomBarId)) {
if (bottomBarId == R.id.home) {
val homeCount =
Collections.frequency(fragmentBackStack, R.id.home)
if (homeCount < 2) {
fragmentBackStack.push(bottomBarId)
} else {
fragmentBackStack.asReversed().remove(bottomBarId)
fragmentBackStack.push(bottomBarId)
}
} else {
fragmentBackStack.remove(bottomBarId)
fragmentBackStack.push(bottomBarId)
}
}
}
needToAddToBackStack = true
}
When navHostFragment
changes the fragment we get a callback to addOnDestinationChangedListener
and we check whether the fragment is already existing in the Stack or not. If not we will add to the top of the Stack, if yes we will swap the position to the Stack's top. As we are now using separate graph for each tab the id in the addOnDestinationChangedListener
and BottomNavigationView will be different, so we use findBottomBarIdFromFragment
to find BottomNavigationView item id from destination fragment.
private fun findBottomBarIdFromFragment(fragmentId:Int?):Int?{
if (fragmentId!=null){
val bottomBarId = when(fragmentId){
R.id.register ->{
R.id.form
}
R.id.leaderboard -> {
R.id.list
}
R.id.titleScreen ->{
R.id.home
}
else -> {
null
}
}
return bottomBarId
} else {
return null
}
}
And when the user clicks back we override the activity's onBackPressed
method(NB:onBackPressed
is deprecated I will update the answer once I find a replacement for super.onBackPressed()
inside override fun onBackPressed()
)
override fun onBackPressed() {
val bottomBarId = if (::navController.isInitialized){
findBottomBarIdFromFragment(navController.currentDestination?.id)
} else {
null
}
if (bottomBarId!=null) {
if (::fragmentBackStack.isInitialized && fragmentBackStack.size > 1) {
if (fragmentBackStack.size == 2 && fragmentBackStack.lastElement() == fragmentBackStack.firstElement()){
finish()
} else {
fragmentBackStack.pop()
val fragmentId = fragmentBackStack.lastElement()
needToAddToBackStack = false
bottomNavigationView.selectedItemId = fragmentId
}
} else {
if (::fragmentBackStack.isInitialized && fragmentBackStack.size == 1) {
finish()
} else {
super.onBackPressed()
}
}
} else super.onBackPressed()
}
When the user clicks back we will pop the last fragment from Stack and set the selected item id in the bottom navigation view.
Upvotes: 0
Reputation: 1755
With the version 2.4.0 of the navigation package, it is finally officially supported!
https://developer.android.com/jetpack/androidx/releases/navigation#version_240_2
Not only that: after uploading the navigation library to this version, this feature is the default behaviour. And as a side note, now this default behaviour includes that fragments are not recreated when navigating among them, that seemed to be something quite requested.
Upvotes: 11
Reputation: 1578
Originally answered here: https://stackoverflow.com/a/63645978/8956093
In Jetpack Navigation Componenet, if you want to perform some operation when fragment is poped then you need to override following functions.
Add OnBackPressedCallback in fragment to run your special operation when back is pressed present in system navigation bar at bottom.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
//perform your operation and call navigateUp
findNavController().navigateUp()
}
}
requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
}
Add onOptionsItemMenu in fragment to handle back arrow press present at top left corner within the app.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
//perform your operation and call navigateUp
findNavController().navigateUp()
return true
}
return super.onOptionsItemSelected(item)
}
If there is no special code to be run when back is pressed on host fragment then use onSupportNavigateUp in Activity.
override fun onSupportNavigateUp(): Boolean {
if (navController.navigateUp() == false){
//navigateUp() returns false if there are no more fragments to pop
onBackPressed()
}
return navController.navigateUp()
}
Note that onSupportNavigateUp() is not called if the fragment contains onOptionsItemSelected()
Upvotes: 0
Reputation: 67248
The key point to have a proper back stack that keeps state's is to have NavHostFragment
's which has childFragmentManager and their own backstack. Extension file of Navigation component's advanced sample actually does this.
/**
* Manages the various graphs needed for a [BottomNavigationView].
*
* This sample is a workaround until the Navigation Component supports multiple back stacks.
*/
fun BottomNavigationView.setupWithNavController(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
): LiveData<NavController> {
// Map of tags
val graphIdToTagMap = SparseArray<String>()
// Result. Mutable live data with the selected controlled
val selectedNavController = MutableLiveData<NavController>()
var firstFragmentGraphId = 0
// First create a NavHostFragment for each NavGraph ID
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Obtain its id
val graphId = navHostFragment.navController.graph.id
if (index == 0) {
firstFragmentGraphId = graphId
}
// Save to the map
graphIdToTagMap[graphId] = fragmentTag
// Attach or detach nav host fragment depending on whether it's the selected item.
if (this.selectedItemId == graphId) {
// Update livedata with the selected graph
selectedNavController.value = navHostFragment.navController
attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
} else {
detachNavHostFragment(fragmentManager, navHostFragment)
}
}
// Now connect selecting an item with swapping Fragments
var selectedItemTag = graphIdToTagMap[this.selectedItemId]
val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId]
var isOnFirstFragment = selectedItemTag == firstFragmentTag
// When a navigation item is selected
setOnNavigationItemSelectedListener { item ->
// Don't do anything if the state is state has already been saved.
if (fragmentManager.isStateSaved) {
false
} else {
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
if (selectedItemTag != newlySelectedItemTag) {
// Pop everything above the first fragment (the "fixed start destination")
fragmentManager.popBackStack(
firstFragmentTag,
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
// Exclude the first fragment tag because it's always in the back stack.
if (firstFragmentTag != newlySelectedItemTag) {
// Commit a transaction that cleans the back stack and adds the first fragment
// to it, creating the fixed started destination.
fragmentManager.beginTransaction()
.attach(selectedFragment)
.setPrimaryNavigationFragment(selectedFragment)
.apply {
// Detach all other Fragments
graphIdToTagMap.forEach { _, fragmentTagIter ->
if (fragmentTagIter != newlySelectedItemTag) {
detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
}
}
}
.addToBackStack(firstFragmentTag)
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim
)
.setReorderingAllowed(true)
.commit()
}
selectedItemTag = newlySelectedItemTag
isOnFirstFragment = selectedItemTag == firstFragmentTag
selectedNavController.value = selectedFragment.navController
true
} else {
false
}
}
}
// Optional: on item reselected, pop back stack to the destination of the graph
setupItemReselected(graphIdToTagMap, fragmentManager)
// Handle deep link
setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)
// Finally, ensure that we update our BottomNavigationView when the back stack changes
fragmentManager.addOnBackStackChangedListener {
if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
this.selectedItemId = firstFragmentGraphId
}
// Reset the graph if the currentDestination is not valid (happens when the back
// stack is popped after using the back button).
selectedNavController.value?.let { controller ->
if (controller.currentDestination == null) {
controller.navigate(controller.graph.id)
}
}
}
return selectedNavController
}
private fun BottomNavigationView.setupDeepLinks(
navGraphIds: List<Int>,
fragmentManager: FragmentManager,
containerId: Int,
intent: Intent
) {
navGraphIds.forEachIndexed { index, navGraphId ->
val fragmentTag = getFragmentTag(index)
// Find or create the Navigation host fragment
val navHostFragment = obtainNavHostFragment(
fragmentManager,
fragmentTag,
navGraphId,
containerId
)
// Handle Intent
if (navHostFragment.navController.handleDeepLink(intent)
&& selectedItemId != navHostFragment.navController.graph.id
) {
this.selectedItemId = navHostFragment.navController.graph.id
}
}
}
private fun BottomNavigationView.setupItemReselected(
graphIdToTagMap: SparseArray<String>,
fragmentManager: FragmentManager
) {
setOnNavigationItemReselectedListener { item ->
val newlySelectedItemTag = graphIdToTagMap[item.itemId]
val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
as NavHostFragment
val navController = selectedFragment.navController
// Pop the back stack to the start destination of the current navController graph
navController.popBackStack(
navController.graph.startDestination, false
)
}
}
private fun detachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment
) {
fragmentManager.beginTransaction()
.detach(navHostFragment)
.commitNow()
}
private fun attachNavHostFragment(
fragmentManager: FragmentManager,
navHostFragment: NavHostFragment,
isPrimaryNavFragment: Boolean
) {
fragmentManager.beginTransaction()
.attach(navHostFragment)
.apply {
if (isPrimaryNavFragment) {
setPrimaryNavigationFragment(navHostFragment)
}
}
.commitNow()
}
private fun obtainNavHostFragment(
fragmentManager: FragmentManager,
fragmentTag: String,
navGraphId: Int,
containerId: Int
): NavHostFragment {
// If the Nav Host fragment exists, return it
val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
existingFragment?.let { return it }
// Otherwise, create it and return it.
val navHostFragment = NavHostFragment.create(navGraphId)
fragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNow()
return navHostFragment
}
private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
val backStackCount = backStackEntryCount
for (index in 0 until backStackCount) {
if (getBackStackEntryAt(index).name == backStackName) {
return true
}
}
return false
}
private fun getFragmentTag(index: Int) = "bottomNavigation#$index"
Important part here is to obtain NavHostFragment if it does not exist in back stack with the function above and add it to back stack. commitNow
is synchronous unlike commit
private fun obtainNavHostFragment( fragmentManager: FragmentManager, fragmentTag: String, navGraphId: Int, containerId: Int ): NavHostFragment { // If the Nav Host fragment exists, return it val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment? existingFragment?.let { return it }
// Otherwise, create it and return it.
val navHostFragment = NavHostFragment.create(navGraphId)
fragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNow()
return navHostFragment
}
I built one using the NavigationExtension above which looks like this
with nested navigation.
Navigation graphs are similar, so only i add one
nav_graph_home.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/nav_graph_home"
app:startDestination="@id/homeFragment1">
<fragment
android:id="@+id/homeFragment1"
android:name="com.smarttoolfactory.tutorial5_3navigationui_bottomnavigation_nestednavigation.blankfragment.HomeFragment1"
android:label="HomeFragment1"
tools:layout="@layout/fragment_home1">
<action
android:id="@+id/action_homeFragment1_to_homeFragment2"
app:destination="@id/homeFragment2" />
</fragment>
<fragment
android:id="@+id/homeFragment2"
android:name="com.smarttoolfactory.tutorial5_3navigationui_bottomnavigation_nestednavigation.blankfragment.HomeFragment2"
android:label="HomeFragment2"
tools:layout="@layout/fragment_home2">
<action
android:id="@+id/action_homeFragment2_to_homeFragment3"
app:destination="@id/homeFragment3" />
</fragment>
<fragment
android:id="@+id/homeFragment3"
android:name="com.smarttoolfactory.tutorial5_3navigationui_bottomnavigation_nestednavigation.blankfragment.HomeFragment3"
android:label="HomeFragment3"
tools:layout="@layout/fragment_home3" >
<action
android:id="@+id/action_homeFragment3_to_homeFragment1"
app:destination="@id/homeFragment1"
app:popUpTo="@id/homeFragment1"
app:popUpToInclusive="true" />
</fragment>
</navigation>
Menu for bottom navigation
menu_bottom_nav.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_graph_home"
android:icon="@drawable/ic_baseline_home_24"
android:title="Home"/>
<item
android:id="@+id/nav_graph_dashboard"
android:icon="@drawable/ic_baseline_dashboard_24"
android:title="Dashboard"/>
<item
android:id="@+id/nav_graph_notification"
android:icon="@drawable/ic_baseline_notifications_24"
android:title="Notification"/>
</menu>
Layout for MainActivity that contains FragmentContainerView
and BottomNavigationView
activiy_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/bottom_nav"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/menu_bottom_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() {
private var currentNavController: LiveData<NavController>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.addOnBackStackChangedListener {
val backStackEntryCount = supportFragmentManager.backStackEntryCount
val fragments = supportFragmentManager.fragments
val fragmentCount = fragments.size
Toast.makeText(
this,
"MainActivity backStackEntryCount: $backStackEntryCount, fragmentCount: $fragmentCount, fragments: $fragments",
Toast.LENGTH_SHORT
).show()
}
if (savedInstanceState == null) {
setupBottomNavigationBar()
} // Else, need to wait for onRestoreInstanceState
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
super.onRestoreInstanceState(savedInstanceState)
// Now that BottomNavigationBar has restored its instance state
// and its selectedItemId, we can proceed with setting up the
// BottomNavigationBar with Navigation
setupBottomNavigationBar()
}
/**
* Called on first creation and when restoring state.
*/
private fun setupBottomNavigationBar() {
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)
val navGraphIds = listOf(
R.navigation.nav_graph_home,
R.navigation.nav_graph_dashboard,
R.navigation.nav_graph_notification
)
// Setup the bottom navigation view with a list of navigation graphs
val controller = bottomNavigationView.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = supportFragmentManager,
containerId = R.id.nav_host_container,
intent = intent
)
// Whenever the selected controller changes, setup the action bar.
controller.observe(this, Observer { navController ->
setupActionBarWithNavController(navController)
})
currentNavController = controller
}
override fun onSupportNavigateUp(): Boolean {
return currentNavController?.value?.navigateUp() ?: false
}
}
Fragment layouts and classes are simple classes so i skipped them out.You can check out full sample i built, or Google's repository to examine extension for advanced navigation or or other samples.
Upvotes: 6
Reputation: 2124
After read your question, I checked Google document again. And I saw that they have provided a solution make Navigation UI work well with BottomNavigationView. So, I created a tutorial for any guys that also need it like me. For text version: https://nhatvm.com/how-to-use-navigationui-with-bottomnavigation-in-android/ And for youtube version: https://youtu.be/2uxILvBbkyY
Upvotes: -1
Reputation: 159
I didn't find any official solutions, but I use my own way
First, I create Stack for handle fragments
needToAddToBackStack : Boolen = true
private lateinit var fragmentBackStack: Stack<Int>
fragmentBackStack = Stack()
and in
navController.addOnDestinationChangedListener { _, destination, _ ->
if (needToAddToBackStack) {
fragmentBackStack.add(destination.id)
}
needToAddToBackStack = true
}
and handle the back button
override fun onBackPressed() {
if (::fragmentBackStack.isInitialized && fragmentBackStack.size > 1) {
fragmentBackStack.pop()
val fragmentId = fragmentBackStack.lastElement()
needToAddToBackStack = false
navController.navigate(fragmentId)
} else {
if (::fragmentBackStack.isInitialized && fragmentBackStack.size == 1) {
finish()
} else {
super.onBackPressed()
}
}
Upvotes: 0
Reputation: 1823
The best solution is the solution that is provided by google team on his repo, the back button still sends u back to first button but the rest behavior its "normal"... It looks strage that Google is still not provinding a good solution, even when they are using it on (youtube, Google Photos, etc), they said androidx its out there to help, but is look like just we go around and find a workaround for the normal stuff.
Here is the link to google Repo where they are using nav. bottom with a navGraph per each button. https://github.com/android/architecture-components-samples/blob/master/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt copy this file into ur project and have a look how it`s implemented in their project. For back button behavior u can create un own stack and onBackpressed just navigate on that stack.
Upvotes: 3
Reputation: 2929
Short and good code in Kotlin to connect bottom navigation items with fragments inside navigation graph:
val navControl = findNavController( R.id.nav_host_frag_main)
bottomNavigationView?.setupWithNavController(navControl)
*Just consider:Bottom navigation id's & fragments inside navigation graph must have the same id. Also thanks to good explanation from @sanat Answer
Upvotes: 2
Reputation: 6389
First, let me clarify how Youtube and Instagram handles fragment navigation.
None of the other answers above solve all this problems using the jetpack navigation.
JetPack navigation has no standard way to do this, the way that I found more simple is to dividing the navigation xml graph into one for each bottom navigation item, handling the back stack between the navigation items myself using the activity FragmentManager and use the JetPack NavController to handle the internal navigation between root and detail fragments (its implementation uses the childFragmentManager stack).
Suppose you have in your navigation
folder this 3 xmls:
res/navigation/
navigation_feed.xml
navigation_explore.xml
navigation_profile.xml
Have your destinationIds inside the navigation xmls the same of your bottomNavigationBar menu ids. Also, to each xml set the app:startDestination
to the fragment that you want as the root of the navigation item.
Create a class BottomNavController.kt
:
class BottomNavController(
val context: Context,
@IdRes val containerId: Int,
@IdRes val appStartDestinationId: Int
) {
private val navigationBackStack = BackStack.of(appStartDestinationId)
lateinit var activity: Activity
lateinit var fragmentManager: FragmentManager
private var listener: OnNavigationItemChanged? = null
private var navGraphProvider: NavGraphProvider? = null
interface OnNavigationItemChanged {
fun onItemChanged(itemId: Int)
}
interface NavGraphProvider {
@NavigationRes
fun getNavGraphId(itemId: Int): Int
}
init {
var ctx = context
while (ctx is ContextWrapper) {
if (ctx is Activity) {
activity = ctx
fragmentManager = (activity as FragmentActivity).supportFragmentManager
break
}
ctx = ctx.baseContext
}
}
fun setOnItemNavigationChanged(listener: (itemId: Int) -> Unit) {
this.listener = object : OnNavigationItemChanged {
override fun onItemChanged(itemId: Int) {
listener.invoke(itemId)
}
}
}
fun setNavGraphProvider(provider: NavGraphProvider) {
navGraphProvider = provider
}
fun onNavigationItemReselected(item: MenuItem) {
// If the user press a second time the navigation button, we pop the back stack to the root
activity.findNavController(containerId).popBackStack(item.itemId, false)
}
fun onNavigationItemSelected(itemId: Int = navigationBackStack.last()): Boolean {
// Replace fragment representing a navigation item
val fragment = fragmentManager.findFragmentByTag(itemId.toString())
?: NavHostFragment.create(navGraphProvider?.getNavGraphId(itemId)
?: throw RuntimeException("You need to set up a NavGraphProvider with " +
"BottomNavController#setNavGraphProvider")
)
fragmentManager.beginTransaction()
.setCustomAnimations(
R.anim.nav_default_enter_anim,
R.anim.nav_default_exit_anim,
R.anim.nav_default_pop_enter_anim,
R.anim.nav_default_pop_exit_anim
)
.replace(containerId, fragment, itemId.toString())
.addToBackStack(null)
.commit()
// Add to back stack
navigationBackStack.moveLast(itemId)
listener?.onItemChanged(itemId)
return true
}
fun onBackPressed() {
val childFragmentManager = fragmentManager.findFragmentById(containerId)!!
.childFragmentManager
when {
// We should always try to go back on the child fragment manager stack before going to
// the navigation stack. It's important to use the child fragment manager instead of the
// NavController because if the user change tabs super fast commit of the
// supportFragmentManager may mess up with the NavController child fragment manager back
// stack
childFragmentManager.popBackStackImmediate() -> {
}
// Fragment back stack is empty so try to go back on the navigation stack
navigationBackStack.size > 1 -> {
// Remove last item from back stack
navigationBackStack.removeLast()
// Update the container with new fragment
onNavigationItemSelected()
}
// If the stack has only one and it's not the navigation home we should
// ensure that the application always leave from startDestination
navigationBackStack.last() != appStartDestinationId -> {
navigationBackStack.removeLast()
navigationBackStack.add(0, appStartDestinationId)
onNavigationItemSelected()
}
// Navigation stack is empty, so finish the activity
else -> activity.finish()
}
}
private class BackStack : ArrayList<Int>() {
companion object {
fun of(vararg elements: Int): BackStack {
val b = BackStack()
b.addAll(elements.toTypedArray())
return b
}
}
fun removeLast() = removeAt(size - 1)
fun moveLast(item: Int) {
remove(item)
add(item)
}
}
}
// Convenience extension to set up the navigation
fun BottomNavigationView.setUpNavigation(bottomNavController: BottomNavController, onReselect: ((menuItem: MenuItem) -> Unit)? = null) {
setOnNavigationItemSelectedListener {
bottomNavController.onNavigationItemSelected(it.itemId)
}
setOnNavigationItemReselectedListener {
bottomNavController.onNavigationItemReselected(it)
onReselect?.invoke(it)
}
bottomNavController.setOnItemNavigationChanged { itemId ->
menu.findItem(itemId).isChecked = true
}
}
Do your layout main.xml
like this:
<androidx.constraintlayout.widget.ConstraintLayout 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">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bottomNavigationView"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Use on your activity like this:
class MainActivity : AppCompatActivity(),
BottomNavController.NavGraphProvider {
private val navController by lazy(LazyThreadSafetyMode.NONE) {
Navigation.findNavController(this, R.id.container)
}
private val bottomNavController by lazy(LazyThreadSafetyMode.NONE) {
BottomNavController(this, R.id.container, R.id.navigation_feed)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
bottomNavController.setNavGraphProvider(this)
bottomNavigationView.setUpNavigation(bottomNavController)
if (savedInstanceState == null) bottomNavController
.onNavigationItemSelected()
// do your things...
}
override fun getNavGraphId(itemId: Int) = when (itemId) {
R.id.navigation_feed -> R.navigation.navigation_feed
R.id.navigation_explore -> R.navigation.navigation_explore
R.id.navigation_profile -> R.navigation.navigation_profile
else -> R.navigation.navigation_feed
}
override fun onSupportNavigateUp(): Boolean = navController
.navigateUp()
override fun onBackPressed() = bottomNavController.onBackPressed()
}
Upvotes: 28
Reputation: 4580
If you have a bottomNavigationView
with 3 items corresponding to 3 Fragment
s: FragmentA
, FragmentB
and FragmentC
where FragmentA
is the startDestination
in your navigation graph, then when you're on FragmentB
or FragmentC
and you click back, you're going to be redirected to FragmentA
, that's the behavior recommended by Google and that's implemented by default.
If however you wish to alter this behavior, you'll need to either use a ViewPager
as was suggested by some of the other answers, or manually handle the fragments backStack and back transactions yourself -which in a way would undermine the use of the Navigation component altogether-.
Upvotes: 0
Reputation: 9277
You have to set host navigation like below xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary" />
<fragment
android:id="@+id/navigation_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
<android.support.design.widget.BottomNavigationView
android:id="@+id/bottom_navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemIconTint="@drawable/color_state_list"
app:itemTextColor="@drawable/color_state_list"
app:menu="@menu/menu_bottom_navigation" />
</LinearLayout>
Setup With Navigation Controller :
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.navigation_host_fragment);
NavigationUI.setupWithNavController(bottomNavigationView, navHostFragment.getNavController());
menu_bottom_navigation.xml :
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@id/tab1" // Id of navigation graph
android:icon="@mipmap/ic_launcher"
android:title="@string/tab1" />
<item
android:id="@id/tab2" // Id of navigation graph
android:icon="@mipmap/ic_launcher"
android:title="@string/tab2" />
<item
android:id="@id/tab3" // Id of navigation graph
android:icon="@mipmap/ic_launcher"
android:title="@string/tab3" />
</menu>
nav_graph.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/nav_graph"
app:startDestination="@id/tab1">
<fragment
android:id="@+id/tab1"
android:name="com.navigationsample.Tab1Fragment"
android:label="@string/tab1"
tools:layout="@layout/fragment_tab_1" />
<fragment
android:id="@+id/tab2"
android:name="com.navigationsample.Tab2Fragment"
android:label="@string/tab2"
tools:layout="@layout/fragment_tab_2"/>
<fragment
android:id="@+id/tab3"
android:name="com.simform.navigationsample.Tab3Fragment"
android:label="@string/tab3"
tools:layout="@layout/fragment_tab_3"/>
</navigation>
By setting up the same id of "nav_graph" to "menu_bottom_navigation" will handle the click of bottom navigation.
You can handle back action using popUpTo
property in action
tag.
Upvotes: 41
Reputation: 12444
I have made an app like this (still not published on PlayStore) that has the same navigation, maybe its implementation is different from what Google does in their apps, but the functionality is the same.
the structure involves I have Main Activity that I switch the content of it by showing/hiding fragments using:
public void switchTo(final Fragment fragment, final String tag /*Each fragment should have a different Tag*/) {
// We compare if the current stack is the current fragment we try to show
if (fragment == getSupportFragmentManager().getPrimaryNavigationFragment()) {
return;
}
// We need to hide the current showing fragment (primary fragment)
final Fragment currentShowingFragment = getSupportFragmentManager().getPrimaryNavigationFragment();
final FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
if (currentShowingFragment != null) {
fragmentTransaction.hide(currentShowingFragment);
}
// We try to find that fragment if it was already added before
final Fragment alreadyAddedFragment = getSupportFragmentManager().findFragmentByTag(tag);
if (alreadyAddedFragment != null) {
// Since its already added before we just set it as primary navigation and show it again
fragmentTransaction.setPrimaryNavigationFragment(alreadyAddedFragment);
fragmentTransaction.show(alreadyAddedFragment);
} else {
// We add the new fragment and then show it
fragmentTransaction.add(containerId, fragment, tag);
fragmentTransaction.show(fragment);
// We set it as the primary navigation to support back stack and back navigation
fragmentTransaction.setPrimaryNavigationFragment(fragment);
}
fragmentTransaction.commit();
}
Upvotes: 4
Reputation: 12005
You don't really need a ViewPager
to work with BottomNavigation
and the new Navigation architecture component. I have been working in a sample app that uses exactly the two, see here.
The basic concept is this, you have the main activity that will host the BottomNavigationView
and that is the Navigation host for your navigation graph, this is how the xml for it look like:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".main.MainActivity">
<fragment
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<android.support.design.widget.BottomNavigationView
android:id="@+id/navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/navigation" />
</android.support.constraint.ConstraintLayout>
The navigation Menu (tabs menu) for the BottomNavigationView
looks like this:
navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@drawable/ic_home"
android:title="@string/title_home" />
<item
android:id="@+id/navigation_people"
android:icon="@drawable/ic_group"
android:title="@string/title_people" />
<item
android:id="@+id/navigation_organization"
android:icon="@drawable/ic_organization"
android:title="@string/title_organization" />
<item
android:id="@+id/navigation_business"
android:icon="@drawable/ic_business"
android:title="@string/title_business" />
<item
android:id="@+id/navigation_tasks"
android:icon="@drawable/ic_dashboard"
android:title="@string/title_tasks" />
</menu>
All of this is just the BottomNavigationView
setup. Now to make it work with the Navigation Arch Component you need to go into the navigation graph editor, add all your fragment destinations (in my case I have 5 of them, one for each tab) and set the id of the destination with the same name as the one in the navigation.xml
file:
This will tell android to make a link between the tab and the fragment, now every time the user clicks the "Home" tab android will take care of loading up the correct fragment.
There is also one piece of kotlin code that needs to be added to your NavHost (the main activity) to wire things up with the BottomNavigationView
:
You need to add in your onCreate:
bottomNavigation.setupWithNavController(Navigation.findNavController(this, R.id.my_nav_host_fragment))
This tells android to do the wiring between the Navigation architecture component and the BottomNavigationView. See more in the docs.
To get the same beahvior you have when you use youtube, just add this:
navigation.setOnNavigationItemSelectedListener {item ->
onNavDestinationSelected(item, Navigation.findNavController(this, R.id.my_nav_host_fragment))
}
This will make destinations go into the backstack so when you hit the back button, the last visited destination will be popped up.
Upvotes: 53
Reputation: 2600
You can have a viewpager setup with bottom navigation view. Each fragment in the viewpager will be a container fragment, it will have child fragments with its own backstack. You can maintain backstack for each tab in viewpager this way
Upvotes: 10