I have a fragment that I define a NavHostFragment inside it like this:
<fragment
android:id="#+id/shipping_host_nav"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/shipping_nav_graph"
app:defaultNavHost="true"/>
when trying to call findNavController method in the fragment it threw an illegal state exception and says that my view group doesn't have a NavController.
java.lang.IllegalStateException: View androidx.core.widget.NestedScrollView{1dd5506 VFED..... ......I. 0,0-0,0} does not have a NavController set
So my question is: can I define a NavHostFragment inside another fragment?
or suitable for activity only?
I have searched a lot to find can I define a nav host fragment inside another fragment but I didn't find any answers.
I have found a solution for this exception, the findNavController() throws this exception when trying to call this method within a fragment that is not NavHostFragment or not within NavHostFragment so I made this mistake by calling this method in my fragment.
So I have to find the controller by myself using Navigation class
Navigation.findNavController(activity, R.id.my_nav_host_fragment)
this is how to find the NavHostFragment (NavController) defined within a fragment
I made an extension function for Fragment class so I can be easily find the nav controller using id
fun Fragment.getFragmentNavController(#IdRes id: Int) = activity?.let {
return#let Navigation.findNavController(it, id)
}
Related
I use Jetpack Navigation for navigating between fragments and activity. So, I have a MainActivity with a FragmentContainerView in the layout. I can easily navigate from fragment to fragment/activity. But, I can't find a way how to navigate from one activity to another activity/fragment with navController.
For example, from fragment FA, I call navController.navigate() to activity A. Now, I want to navigate from activity A to activity B or fragment FB.
I already tried this:
val host = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = host.navController
But always got this error
null cannot be cast to non-null type androidx.navigation.fragment.NavHostFragment
Thanks!
A note from the navigation component guide
Note: The Navigation component is designed for apps that have one main activity with multiple fragment destinations. The main activity is associated with a navigation graph and contains a NavHostFragment that is responsible for swapping destinations as needed. In an app with multiple activity destinations, each activity has its own navigation graph.
You shouldn't use the navigation component to navigate from one activity to another. It is made to swap fragments on a Fragment container.
You can get navController inside activity using
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
You also have to add android:name="androidx.navigation.fragment.NavHostFragment" to your fragment container
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
For java coder;
it is possible to get NavController instance this way
NavHostFragment navHostFragment =(NavHostFragment)getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
I have this tabbed UI with a navigation component and a BottomNavigationView that handles the tabs.
My use case involves one of these tab fragments to have a BottomNavigationView of its own.
I don't think the nested navigation graphs will work for me because I need there to be an inner NavHostFragment and a second BottomNavigationView.
So I'm in the fragment which I wish to host my inner navigation graph. It contains a fragment like so.
<androidx.fragment.app.FragmentContainerView
android:id="#+id/inner_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
I need a way to get the navigation controller for the above fragment.
Normally when you're in an activity you'd call the findFragmentById from the supportFragmentManager with the id you gave to your fragment.
val navHostFragment = supportFragmentManager.findFragmentById(R.id.outer_nav_host) as NavHostFragment
So I tried to call that on the activity object from within my inner fragment
requireActivity().supportFragmentManager.findFragmentById(R.id.inner_host_fragment)
But the findFragmentById returns null.
Try to get the NavHostFragment from the supportFragmentManager, and then get the navController upon.
val navHostFragment =
requireActivity().supportFragmentManager.primaryNavigationFragment as NavHostFragment
val navController = navHostFragment.navController
Thanks to #ianhanniballake you need to use the childFragmentManager as your current fragment is a child of a fragment.
So try to use instead:
val navHostFragment =
childFragmentManager.primaryNavigationFragment as NavHostFragment
val navController = navHostFragment.navController
UPDATE:
throws this java.lang.NullPointerException: null cannot be cast to non-null type androidx.navigation.fragment.NavHostFragment
So, the solution as from comments:
val navHostFragment = childFragmentManager.findFragmentById(R.id.nav_host_fragment)
as NavHostFragment
And replace nav_host_fragment with the id of the inner NavHostFragment.
When a configuration change happens and my Activity and Fragment are recreated because of it, my nav Graph scoped ViewModel is unavailable while the Fragments have already been created again.
It seems like the Fragment gets recreated, before the navGraph does.
I am using this code to initialize my navGraph scoped ViewModel from my Fragment:
private val myViewModel: MyViewModel by navGraphViewModels(R.id.nav_graph_id)
If I try to use myViewModel in the Fragments onViewCreated function, I get a IllegalArgumentException after a Configuration change. The Exception:
java.lang.IllegalArgumentException: No destination with ID <destination id> is on the NavController's back stack
How do I handle this?
I have already checked that my ID isn't used anywhere else.
Edit1:
Here is my activity_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/main_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:navGraph="#navigation/main_nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
And here is my main_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/main_nav_graph"
app:startDestination="#id/main_nav_graph_1">
<navigation
android:id="#+id/main_nav_graph_1"
android:label="#string/nav_graph_1_label"
app:startDestination="#id/nav_graph_1_start_fragment">
<!-- nav graph stuff -->
</navigation>
<navigation
android:id="#+id/main_nav_graph_2"
android:label="#string/nav_graph_2_label"
app:startDestination="#id/nav_graph_2_start_fragment">
<fragment
android:id="#+id/nav_graph_2_start_fragment"
android:label="#string/nav_graph_2_start_fragment_label"
android:name="my.package.ui.NavGraph2StartFragment"
tools:layout="#layout/fragment_nav_graph_2_start">
</fragment>
<!-- In here is where I get the problem -->
</navigation>
</navigation>
My Problem was the following:
After a Configuration change the NavGraph returned to its start destination, but the Fragment that was last active gets loaded anyway. This meant that the Fragment that was actually started and the current destination of the navGraph were out of sync.
When my Fragment then tried to load the navGraph scoped ViewModel, it failed because the navGraph thought it was on a different Fragment then it actually was.
To fix my problem I had to save and restore the state of the navController using the activities savedInstanceState Bundle. (source: https://stackoverflow.com/a/59987336)
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
savedInstanceState.putBundle("nav_state", fragment.findNavController().saveState())
}
// restore in RestoreInstanceState
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
fragment.findNavController().restoreState(savedInstanceState.getBundle("nav_state"))
}
// or restore in onCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState?.containsKey("nav_state") == true) {
fragment.findNavController().restoreState(savedInstanceState.getBundle("nav_state"))
}
}
For anyone else coming here, I experienced this issue when a dialog was currently displayed and I issued a navController.navigate() call to a Fragment which uses by navGraphViewModels() before the dialog was dismissed.
It's as if the Navigation library couldn't understand that there is a destination with that ID available when a dialog is being displayed.
And for more clarity, this dialog is displayed using the <dialog> attribute in my nav graph XML.
My fix (for now) is to ensure the dialog is dismissed before making a further call to navigate elsewhere by way of view.postDelayed({ navController.navigate(elsewhere)}, 300)
This isn't the first time I've experienced issues with the NavController trying to navigate when a dialog is displayed.
Solved Easy by change this :
private val myViewModel: MyViewModel by hiltNavGraphViewModels(R.id.nav_graph_id)
To:
private val viewModel: AuthViewModel by activityViewModels{ defaultViewModelProviderFactory }
I need to transfer data from one fragment to another. Now the recommended way to do this is to use a shared ViewModel. To get the same instance available in both fragments, common owner is needed. As it can be their common Activity. But with this approach (In the case of Single Activity), the ViewModel instance will live throughout the entire application. In the classic use of fragments, you can specify ViewModelProvider (this) in the parent fragment, and ViewModelProvider (getParentFramgent ()) in the child. Thus, the scope of ViewModel is limited to the life of the parent fragment. The problem is that when using Navigation Component, getParentFramgent () will return NavHostFragment, not the parent fragment. What do I need to do?
Code samples:
Somewhere in navigation_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"
app:startDestination="#id/mainMenuFragment">
<fragment
android:id="#+id/mainMenuFragment"
android:name="com.mypackage.mainmenu.MainMenuFragment"
android:label="MainMenu"
tools:layout="#layout/fragment_main_menu">
<action
android:id="#+id/start_game_fragment"
app:destination="#id/gameNav" />
</fragment>
<navigation
android:id="#+id/gameNav"
app:startDestination="#id/gameFragment">
<fragment
android:id="#+id/gameFragment"
android:name="com.mypackage.game.GameFragment"
android:label="#string/app_name"
tools:layout="#layout/fragment_game"/>
</navigation>
</navigation>
Somewhere in MainMenuFragment:
override fun startGame(gameSession: GameSession) {
//This approach doesn't work
ViewModelProvider(this)[GameSessionViewModel::class.java].setGameSession(
gameSession
)
findNavController().navigate(R.id.start_game_fragment)
}
GameFragment:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
gameSessionViewModel =
ViewModelProvider(requireParentFragment())[GameSessionViewModel::class.java].apply {
val session = gameSession.value
)
}
}
EDIT:
I can use NavHostFragment(returned from getParentFragment()) as a common for all fragments, but then, as in the case of Activity, ViewModel.onCleared() will not be called when the real parent fragment finishes.
There's really no way to do this.
Here is a code snippet from androidx.navigation.fragment.FragmentNavigator:
public NavDestination navigate(#NonNull Destination destination, #Nullable Bundle args,
#Nullable NavOptions navOptions, #Nullable Navigator.Extras navigatorExtras) {
// ...
final FragmentTransaction ft = mFragmentManager.beginTransaction();
// ...
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
// ...
}
Under the hood, the FragmentManager is used, which calls replace(). Therefore, the child fragment is not added, but is replaced with a new one, so it will not be in getParentFramgent().
I faced the same problem and after researching and experimenting, I found the solution.
You simply have to call this while scoping your ViewModel which will resolve to your fragment where you have created the navigation host fragment.
ViewModelProvider(getParentFragment().getParentFragment())
or more appropriately
ViewModelProvider(requireParentFragment().requireParentFragment())
(to avoid NPE)
This is because child fragment's parent is NavHostFragment and NavHostFragment's parent is ParentFragment.
I tested this and it's working fine for me
for those of you who are bothered with this problem, here is the answer that worked for me:
suppose that you go from fragment A to fragment B, and you want to use a shared ViewModel for fragment A and B, and in this case, A is a fragment and B is a dialogFragment.
you can initialize ViewModel in fragment A:
ViewModelProvider(this).get(AviewModel::class.java)
and in order to use it on fragment B, initialize the ViewModel in fragment B like this:
ViewModelProvider(requireParentFragment().childFragmentManager.fragments[0]).get(AviewModel::class.java)
TL;DR:
requireParentFragment() returns NavHostFragment which hosts all the navigation of the current navigation.
requireParentFragment().childFragmentManager returns FragmentManagerImpl which seems to be the container for all fragments.
requireParentFragment().childFragmentManager.fragments returns a mutable list of fragments that are in the stack, so you can iterate through the list to see the fragments that are living in stack.
I have two graphs, so the first graph move from one fragment to an activity passing safeArgs to the activity.
val action = MyFragmentDirections.actionMyActivity(arg1, arg2)
Navigation.findNavController(view).navigate(action)
Now in the second, I want to pass these arguments from MyActivity to a fragment which belongs to this activity.
I can get the args:
val args = MyActivity.fromBundle(intent.extras)
The problem is there is not a Directions file for this activity, so I can't pass the arguments.
Navigation 1.0.0-alpha07 fixed the feature request for passing arguments to the start destination of a graph.
To use this, you'd need to:
Remove the app:navGraph attribute from your NavHostFragment
Call findNavController(R.id.your_nav_host_fragment).setGraph(R.navigation.your_graph, intent.extras)
Using the R.id of your NavHostFragment and R.navigation that you previously had on your app:navGraph tag. By passing the arguments into the setGraph call, your starting destination will get the arguments directly, without calling navigate again (which would, by default, create a new instance of the destination on your back stack - not what you want).
I don't know if this is recommended, but it is working:
val args = MyActivity.fromBundle(intent.extras)
navController.navigate(R.id.myActivityFragment, args.toBundle())
It work for me
Remove the app:navGraph attribute from your NavHostFragment
<androidx.fragment.app.FragmentContainerView
android:id="#+id/video_view_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="false"
app:defaultNavHost="true"
app:layout_constraintTop_toTopOf="parent"
/>
and call
navHostFragment = supportFragmentManager
.findFragmentById(R.id.video_view_fragment) as NavHostFragment
navHostFragment.navController.setGraph(R.navigation.video_navigation, intent.extras)