Navigation Component: pass safeArgs from activity to a fragment - android

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)

Related

How to get navController inside activity?

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();

NavigationComponent: NavHostFragment inside a fragment

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.

Get parent fragment from child when using Navigation Component

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.

Defining a NavHostFragment inside another fragment

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

How to map NavArgs to NavArgument?

The issue I encountered is related to setting navigation graph programatically. What I want to achieve is to decide in Activity which fragment should be a start destination. What's more, each of these fragments have additional arguments.
Let's say we have such 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"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph">
<fragment
android:id="#+id/fragment1"
android:name="com.mypackage.Fragment1"
tools:layout="#layout/layout_fragment1">
<argument
android:name="argument1"
app:argType="int" />
</fragment>
<fragment
android:id="#+id/fragment2"
android:name="com.mypackage.Fragment2"
tools:layout="#layout/layout_fragment2">
<argument
android:name="argument2"
app:argType="string" />
</fragment>
</navigation>
If we use Safe Args plugin (androidx.navigation.safeargs.kotlin), two classes will be generated: Fragment1Args and Fragment2Args, both implementing NavArgs interface.
Now because we need to decide which fragment should became start destination, we need to make graph programatically:
val graph = navController.navInflater.inflate(R.navigation.nav_graph)
if (someCondition) {
graph.startDestination = R.id.fragment1
} else {
graph.startDestination = R.id.fragment2
}
navController.graph = graph
But Fragment1 and Fragment2 requires some additional arguments, so we need somehow to add them to graph. It turns out that graph has method addArguments(NavArguments), but as you see it's not NavArgs interface. Question is how to set these arguments properly that Fragment1 or Fragment2 will be able to extract?
I'm using 1.0.0-rc02 version of navigation framework.
When you add <argument> tag to your navigation XML, it is already creating the NavArgument classes when you call inflate(), so there's nothing you need to do to the graph in order to pass arguments to the start destination.
Instead, you should use setGraph(NavGraph, Bundle) to set the graph and pass initial arguments to the start destination.
// Construct your Bundle of arguments
val bundle = bundleOf()
// set the graph with specific arguments for the start destination
navController.setGraph(graph, bundle)
You said: "What I want to achieve is to decide in Activity which fragment should be a start destination."
And I think you're trying to make the decision in the fragmen, which is not correct here because you didn't open the fragment yet, Thus the correct behavior is to do that in activity e.g:
when (someCondition) {
TO_FRAGMENT_1-> {
findNavController(R.id.fragment).navigate(R.id.fragment1)
}
TO_FRAGMENT_2-> {
findNavController(R.id.fragment).navigate(R.id.fragment2)
}
}
And if the activity is the first activity in your app, you should use something else instead of args, e.g: SharedPref.

Categories

Resources