Currently I have the following scenario: a user must sign in to use the app. This means I've used 2 nav_graphs, a main graph for everything and then a nested home graph for the views after you have signed in.
After signing in, a bottom navigation bar should appear to change tabs in the home graph.
I have the following home_fragment.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.home.HomeFragment">
<fragment
android:id="#+id/home_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/home_graph"
app:defaultNavHost="true"/>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_navigation"
app:menu="#menu/navigation_items"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And I want to be able to change tabs in the bottom navigation view, so I setup this logic in HomeFragement.kt using bottomNavigationView.setOnNavigationItemSelectedListener.
Unfortunetly, when I try to fetch the home_nav_host_fragment in home_fragment.xml I cannot because homeNavController = findNavController() in the fragment can only find the main_nav_host_fragment that's in the main activity.
I want findNavController() to return the home_nav_host_fragment instead but because this method only searches for parent NavControllers and not ones on the same level it cannot find the one I want.
Is there a better structure that will provide a solution to this issue? Thanks
This isn't the correct approach. Instead, you should listen for navigation events and hide the BottomNavigationView when you're on the login graph:
navController.addOnDestinationChangedListener { _, destination, _ ->
if(destination.parent!!.id == R.id.login_graph) {
toolbar.visibility = View.GONE
bottomNavigationView.visibility = View.GONE
} else {
toolbar.visibility = View.VISIBLE
bottomNavigationView.visibility = View.VISIBLE
}
}
Then, you can use just a single graph, following the best practices for user login.
Better use popUpTo combined with popUpToInclusive to create a one-way navigation action - in order to use a single one graph, instead of two disconnected graphs. This only should be applied to the first 1-2 fragments in the graph, so that eg. the back button cannot navigate back to the splash-screen or to the login-screen, because these actions do not count towards "the expected behavior".
<fragment
android:id="#+id/splashFragment"
android:name="com.acme.fragment.SplashFragment"
tools:layout="#layout/fragment_splash"
android:label="fragment_splash">
<action
android:id="#+id/action_splashFragment_to_loginFragment"
app:destination="#id/loginFragment"
app:popUpTo="#id/splashFragment"
app:popUpToInclusive="true"/>
</fragment>
Related
I'm currently working on a multi-module application which is structured as below and I can't get the back navigation to function correctly when navigating between feature modules. Either nothing happens or the app closes instead of just going back to the previous destination.
- app
- feature-explore
- feature-search
- feature-detail
Each of the feature modules has it's own navigation graph which is brought together in the main navigation graph located in the app module. The app module contains a single MainActivity with a FragmentContainerView and BottomNavigationView in its layout...
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/main_bottomnav"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/menu_bottomnav" />
<androidx.fragment.app.FragmentContainerView
android:id="#+id/main_navhost"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/nav_main" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
The BottomNavigationView is linked to a menu to hook up feature-explore and feature-search modules as main destinations with their own navigation graphs...
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
navController = (supportFragmentManager.findFragmentById(R.id.main_navhost) as NavHostFragment).navController
binding.mainBottomnav.setupWithNavController(
navController
)
}
}
I've then set up a global action in the main navigation graph to allow each of the feature-explore and feature-search modules to navigate to the feature-detail module (e.g. clicking an item in a list opens up the detail of that item via a separate feature module).
This is the main navigation graph...
<?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"
android:id="#+id/nav_main"
app:startDestination="#id/nav_explore">
<include app:graph="#navigation/nav_detail" />
<include app:graph="#navigation/nav_explore" />
<include app:graph="#navigation/nav_search" />
<action
android:id="#+id/action_global_nav_detail"
app:destination="#id/nav_detail">
<argument
android:name="id"
app:argType="long" />
</action>
</navigation>
Switching between menu items works fine as does navigating using the global action above to move to the nav_detail graph.
From the fragment in that nav_detail graph though I would expect the device back button to take the user back to the previous view in the back stack. Instead nothing happens and the user stays on the detail fragment (the start destination of nav_detail).
If I update the global action to specify a popUpTo value then MainActivity is closed altogether...
<action
android:id="#+id/action_global_nav_detail"
app:destination="#id/nav_detail"
app:popUpTo="#id/nav_explore"
app:popUpToInclusive="false">
<argument
android:name="id"
app:argType="long" />
</action>
Anyone have any suggestions on how the get the back navigation from nav_detail to behave as expected?
Note - I'm using Navigation Components v2.5.3.
After further investigation I found that the issue wasn't actually to do with the navigation itself. The back press was actually going back to the previous fragment.
I was however using LiveData to hold an ID of the selected item and, as the view model and the LiveData value was retained, it was observed again immediately after navigating back and therefore navigated to the detail fragment again.
See similar issue here with a link to options for changing approach with the LiveData to avoid this problem.
I am currently working on a school project that displays your timetable, grades, subjects and so on. To use any functionality in the Android application you have to be logged in. Here comes my problem:
When the users starts the application for the first time, they should see a login fragment. Once the login is completed they will be presented with a setup screen where they can choose color themes for grades and other user specific things. Only then should they be presented with the actual main fragment. On the second start the user should directly see the main fragment
The main fragment is a FragmentContainerView and a BottomNavigationBar that hosts 5 other fragments. In each of those subfragments you can click on items. Then a different fragment should be presented that shows some more details. These fragments however should overlap the bottom navigation so that you have to navigate back before you can choose a different fragment in the bottom navigation bar.
As far as I can tell I need nested FragmentContainerViews. The MainActivity should be
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
The MainActivity should host the LoginFragment, SetupFragment and a MainFragment. In the MainFragment should be like this
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="#menu/bottom_nav_menu" />
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="#+id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
I would like to achieve all of this with a navigation graph. Do I have to use one graph or two for each FragmentContainerView? Then there is also the problem with the detail Fragments. If for example I have a HomeFragment as a child of the MainFragment which is a child of the MainActivity and in the HomeFragment the user clicks on for example a button to the SettingsFragment, this fragment should be displayed as a child of the MainActivity. How would I get this working? I've already had a look at this question but don't really understand how to implement it.
How to setup Multiple nested FragmentContainerViews with respective navigation graphs?
Could somebody maybe create a very simple implementation of this. I especially need help with the nested FragmentContainerViews, the BottomNavigationBar and the NavigationGraph. I've also come across the problem that the bottom navigation bar doesn't respond anymore.
Thanks for your help in advance. Please let me know if you need any more details.
1.way
val homeFragment = HomeFragment()
val listFragment = ListFragment()
val profileFragment = ProfileFragment()
nav_view.setOnItemSelectedListener {
when (it) {
R.id.homeFragment -> setCurrentFragment(homeFragment)
R.id.listFragment -> setCurrentFragment(listFragment)
R.id.profileFragment ->
setCurrentFragment(profileFragment)
}
return#setOnItemSelectedListener
}
private fun setCurrentFragment(fragment: Fragment) =
supportFragmentManager.beginTransaction().apply {
replace(R.id.nav_host_fragment_activity_main, fragment)
addToBackStack(null) //If you delete this, press the back button, it will not return to the previous fragment.
commit()
}
2.way
Navigation with
https://www.youtube.com/watch?v=DI0NIk-7cz8&t=527s&ab_channel=Stevdza-San
I've a multi-graph navigation app and I'd like to switch between graphs by using a global action as defined in my root main_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_graph"
app:startDestination="#id/loadingFragment">
<include app:graph="#navigation/learn_graph" />
<action
android:id="#+id/action_global_learn_graph"
app:destination="#id/learn_graph"
app:launchSingleTop="true"
/>
</navigation>
Since I'm trying to switch between graphs, I'd like to clear the back stack from the fragments loaded by the source graph (main_graph) when navigating the global action to the destination graph (explore_graph). The expected behavior would be to navigate to the startDestination fragment of the destination graph keeping only that fragment in the backstack.
For normal actions (actions in the same graph) I'm able to use popUpTo flag, how it's possible to get the same behavior for a global action?
After a lot of attemps, I found out a solution. The base idea is to pop up the backstack to the graph that "owns" the global action. In my case main_graph is the owner, so I did:
<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_graph"
app:startDestination="#id/loadingFragment">
<include app:graph="#navigation/learn_graph" />
<action
android:id="#+id/action_global_learn_graph"
app:destination="#id/learn_graph"
app:popUpTo="#+id/main_graph"
app:launchSingleTop="true" />
</navigation>
In addition, you have to set the app:launchSingleTop flag to true in order to make the instance of destination graph unique in your backstack
You can also include app:popUpToInclusive="true" to indicate that the destination specified in app:popUpTo should also be removed from the back stack.
While using navigation architecture as from here , here clearTask is deprecated.
My scenario is this: There are 2 screens Login and Registration, both have links to each other. So you can go to Registration from Login and also Login from Registration. But on back Press App should be closed.
It could simply be done by just adding clearTask to both actions as below.
<?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/nv_auth_phase"
app:startDestination="#id/fragment_login">
<fragment
android:id="#+id/fragment_login"
android:name="com.jobhook.ui.auth.login.LoginFragment"
android:label="LoginFragment"
tools:layout="#layout/fragment_login">
<action
android:id="#+id/nv_action_login_to_registration"
app:clearTask="true"
app:destination="#id/fragment_registration" />
</fragment>
<fragment
android:id="#+id/fragment_registration"
android:name="com.jobhook.ui.auth.registration.RegistrationFragment"
android:label="RegistrationFragment"
tools:layout="#layout/fragment_registration">
<action
android:id="#+id/nv_action_registration_to_login"
app:clearTask="true"
app:destination="#id/fragment_login" />
</fragment>
</navigation>
But as it was deprecated I have tried other solution like adding popUpTo -> navigation graph's Id, making launchSingleTop to true in both actions. Nothing seems to work in my scenario.
I have checked this question also but didn't get a solution.
You need to use in your action next code
app:popUpTo="#+id/fragment_login"
app:popUpToInclusive="true"
Set your NavHostFragment defaultNavHost value false,
<fragment
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="false"
app:navGraph="#navigation/nav_graph"
... />
The app:defaultNavHost="true" attribute ensures that your NavHostFragment intercepts the system Back button. Note that only one NavHost can be the default. If you have multiple hosts in the same layout (two-pane layouts, for example), be sure to specify only one default NavHost.
Simple and effective solution:
fun signOut(activity: Activity) = activity.finish()
I'm using Jetpack Navigation version 1.0.0-alpha04 with bottom navigation. It works but the navigation doesn't happen correctly. For example, if I have tab A and tab B and from tab A I go to Page C and from there I go to tab B and come back to tab A again, I will see root fragment in the tab A and not page C which does not what I expect.
I'm looking for a solution to have a different stack for each tab, so the state of each tab is reserved when I come back to it, Also I don't like to keep all this fragment in the memory since it has a bad effect on performance, Before jetpack navigation, I used this library https://github.com/ncapdevi/FragNav, That does exactly what, Now I'm looking for the same thing with jetpack navigation.
EDIT 2: Though still no first class support (as of writing this), Google has now updated their samples with an example of how they think this should be solved for now: https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample
The major reason is you only use one NavHostFragment to hold the whole back stack of the app.
The solution is that each tab should hold its own back stack.
In your main layout, wrap each tab fragment with a FrameLayout.
Each tab fragment is a NavHostFragment and contains its own navigation graph in order to make each tab fragment having its own back stack.
Add a BottomNavigationView.OnNavigationItemSelectedListener to BottomNavigtionView to handle the visibility of each FrameLayout.
This also takes care of your "...I don't like to keep all this fragment in memory...", because a Navigation with NavHostFragment by default uses fragmentTransaction.replace(), i.e. you will always only have as many fragments as you have NavHostFragments. The rest is just in the back stack of your navigation graph.
Edit: Google is working on a native implementation https://issuetracker.google.com/issues/80029773#comment25
More in detail
Let's say you have a BottomNavigationView with 2 menu choices, Dogs and Cats.
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="#+id/dogMenu"
.../>
<item android:id="#+id/catMenu"
.../>
</menu>
Then you need 2 navigation graphs, say dog_navigation_graph.xml and cat_navigation_graph.xml.
The dog_navigation_graph might look like
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/dog_navigation_graph"
app:startDestination="#id/dogMenu">
</navigation>
and the corresponding for cat_navigation_graph.
In your activity_main.xml, add 2 NavHostFragments
<FrameLayout
android:id="#+id/frame_dog"
...>
<fragment
android:id="#+id/dog_navigation_host_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/dog_navigation_graph"
app:defaultNavHost="true"/>
</FrameLayout>
and underneath add the corresponding for your cat NavHostFragment. On your cat frame layout, set android:visibility="invisible"
Now, in your MainActivity's onCreateView you can
bottom_navigation_view.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.dogMenu -> showHostView(host = 0)
R.id.catMenu -> showHostView(host = 1)
}
return#setOnNavigationItemSelectedListener true
}
All that showHostView() is doing is toggling the visibility of your FrameLayouts that are wrapping the NavHostFragments. So make sure to save them in some way, e.g. in onCreateView
val hostViews = arrayListOf<FrameLayout>() // Member variable of MainActivity
hostViews.apply {
add(findViewById(R.id.frame_dog))
add(findViewById(R.id.frame_cat))
}
Now it's easy to toggle which hostViews should be visible and invisible.
The issue has been resolved by the Android team in the latest version 2.4.0-alpha01 multiple backstacks along with bottom navigation support is now possible without any workaround.
https://developer.android.com/jetpack/androidx/releases/navigation
First, I want to make an edit to #Algar's answer. The frame that you want to hide should have android:visibility="gone" instead of invisible. The reason for that in your main layout you would have something like this:
<LinearLayout 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"
android:orientation="vertical"
tools:context=".ui.activity.MainActivity">
<include
android:id="#+id/toolbar"
layout="#layout/toolbar_base" />
<FrameLayout
android:id="#+id/frame_home"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
>
<fragment
android:id="#+id/home_navigation_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/home_nav" />
</FrameLayout>
<FrameLayout
android:id="#+id/frame_find"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:visibility="gone">
<fragment
android:id="#+id/find_navigation_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/find_nav" />
</FrameLayout>
...
</LinearLayout>
If you wrap your main in a LinearLayout, setting the frame to invisible still make that frame counts, so the BottomNavigation wont appear.
Second, you should create a NavHostFragment instance (ie: curNavHostFragment) to keep track of which NavHostFragment is being visible when a tab in BottomNavigation is clicked. Note: you may want to restore this curNavHostFragment when the activity is destroyed by configuration's changes. This is an example:
#Override
protected void onRestoreInstanceState(#NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
//if this activity is restored from previous state,
//we will have the ItemId of botnav the has been selected
//so that we can set up nav controller accordingly
switch (bottomNav.getSelectedItemId()) {
case R.id.home_fragment:
curNavHostFragment = homeNavHostFragment;
...
break;
case R.id.find_products_fragment:
curNavHostFragment = findNavHostFragment;
...
break;
}
curNavController = curNavHostFragment.getNavController();