I've started using Android Architecture Components (Navigation and Safe Args, View Models) along with Koin library.
Currently, I've got a problem with passing arguments between two fragments - I need to pass a string value from fragment A to fragment B, modify this value in fragment B and pass it back to fragment A.
I've found one possible solution to my problem - shared view models. Unfortunately, this approach has one problem because I can pass and modify values between screens, but when the fragment A navigate to another destination the value in the shared view model is still stored and not cleared.
Is there any different solution of passing and modifying data between fragments in Android Navigation? I want to avoid clearing this one value by hand (when the fragment A is destroyed).
Android just released a solution for this; Passing data between Destinations (Navigation 2.3.0-alpha02), basically, in fragment A you observe changes in a variable and in fragment B you change that value before executing popBackStack().
Fragment A:
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>("key")?.observe(viewLifecycleOwner) { result ->
// Do something with the result.
}
Fragment B:
navController.previousBackStackEntry?.savedStateHandle?.set("key", result)
navController.popBackStack()
You can use Fragment Result API.
Fragment A -> Fragment B
In Fragment A :
binding.buttonGo.setOnClickListener {
setFragmentResultListener(ADD_LOCATION) { key, bundle ->
clearFragmentResultListener(requestKey = ADD_LOCATION)
val selectedLocationModel =
bundle.getParcelable<LocationModel>(SELECTED_LOCATION_MODEL)
this.selectedLocationModel = selectedLocationModel
}
navToFragmentB()
}
In Fragment B:
setFragmentResult(
ADD_LOCATION,
bundleOf(SELECTED_LOCATION_MODEL to selectedLocationModel)
)
goBack()
Do not forget to call clearFragmentResultListener() before create new one.
Currently, I've got a problem with passing arguments between two fragments - I need to pass a string value from fragment A to fragment B, modify this value in fragment B and pass it back to fragment A.
The theoretical solution really is to have the two fragments in a shared <navigation tag, then scope the ViewModel to the ID of the navigation tag, this way you now share the ViewModel between the two screens.
To make this reliable, it's best to use the NavBackStackEntry of the Navigation tag as both a ViewModelStoreOwner and SavedStateRegistryOwner, and create an AbstractSavedStateViewModelFactory that will create the ViewModel using the ViewModelProvider, while also giving you a SavedStateHandle.
You can communicate the results from FragmentB to FragmentA using this SavedStateHandle, associated with the shared ViewModel (scoped to the shared NavGraph).
You can try this solution
<fragment
android:id="#+id/a"
android:name="...">
<argument
android:name="text"
app:argType="string" />
<action
android:id="#+id/navigate_to_b"
app:destination="#id/b" />
</fragment>
<fragment
android:id="#+id/b"
android:name="...">
<argument
android:name="text"
app:argType="string" />
<action
android:id="#+id/return_to_a_with_arguments"
app:destination="#id/a"
app:launchSingleTop="true"
app:popUpTo="#id/b"
app:popUpToInclusive="true" />
</fragment>
and navigation fragment
NavHostFragment.findNavController(this).navigate(BFragmentDirections.returnToAWithArguments(text))
ianhanniballake`s comment has helped me solve a similar problem
1) Pass string from Fragment A to Fragment B with action_A_to_B and SafeArgs.
2) popBackStack to remove Fragment B.
navController.popBackStack(R.id.AFragment, false);
or
navController.popBackStack();
3) Then pass modified data from B to A with action_B_to_A.
EDIT.
Here you have some another solution
Related
I have a question. I'm using nav component for navigation. For example i have fragment A, B and C and bottomNavigation. I'm using
binding.bottomNavigation.setupWithNavController(navController)
For multiple backstack. But here is situation: Main frag is A. I'm moving to fragment B or C. I have buttons on fragments B and C which should lead me to fragment A with putted arguments in it so i'm using just:
findNavController().navigate(fragmentBDirections.fromFragmentBToFragmentA(argument))
But here is a problem. I'm recreating fragment A after this but i'm already have this fragment in backstack. So is it possible to find A in backstack and navigate to it without recreating? Is it possible to save backstack after that?
Your problem seems like an ideal case to use a sharedViewModel
Your button in B or C should pop and fallback to A after updating a property in the viewModel. On leaving, the viewModel is not destroyed because it is bound to the activity and is available for Fragment A.
Bonus is using LiveData so that the change is observed and updated automatically
I think SavedStateHandle might be helpful.
<Navigation>
<fragment
android:id="#+id/BFragment"
android:name="com.packageName.app.BFragment"
android:label="fragment_b"
tools:layout="#layout/fragment_b" >
<action
android:id="#+id/action_BFragment_pop_including_AFragment"
app:popUpTo="#id/AFragment"
app:launchSingleTop="true" />
</Navigation>
<Navigation>
<fragment
android:id="#+id/CFragment"
android:name="com.packageName.app.CFragment"
android:label="fragment_c"
tools:layout="#layout/fragment_c" >
<action
android:id="#+id/action_CFragment_pop_including_AFragment"
app:popUpTo="#id/AFragment"
app:launchSingleTop="true" />
</Navigation>
try it . it's my solution
I am using Navigation Component in my project and :
I need to open a fragment in a different level of hierarchy, so that the back stack is created properly too
in my nav_graph.xml there is a hierarchy like this:
HomeFragment -> CollocationFragment -> ChapterFragment --[selfNavigate]--> ChapterFragment -> PlayerFragment
as you see one of my fragments navigates to itself and send an arguments each time like this:
<fragment
android:id="#+id/ChapterFragment ">
<action
android:id="#+id/action_chapterFragment_self"
app:destination="#id/ChapterFragment" />
<argument
android:name="chapterID"
app:argType="Long" />
</fragment>
in a nutshell, I need to open PlayerFragment from HomeFragment with an appropriate backstack (the hierarchy mentioned above) .
HomeFragment ---[my back stack]---> PlayerFragment
I know that it seems NavDeepLinkBuilder creates the backstack itself but I have no idea how to create a custom backstack by using Navigation component for my fragments in this case.
eventually I find a way...it's kind of a workaround but it works like a charm:
so, just call navigate function in your desire order to make the hierarchy backstack:
fun openPlayerFromHome(){
findNavController.navigate(R.id.HomeFragment)
findNavController.navigate(R.id.CollocationFragment)
findNavController.navigate(R.id.ChapterFragment,bundleOf("chapterID",id))
findNavController.navigate(R.id.PlayerFragment)
}
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.
Imagine that FragmentA starts FragmentB on a button click. Passing some arguments to FragmenB is pretty straightforward:
val action = FragmentADirections.actionToFragmentB("some text")
findNavController().navigate(action)
But is there a way to pass data from FragmentB to FragmentA when the user navigates away from FragmentB? With other words, when FragmentB is destroyed as the result of back press.
In your Navigation UI xml:
<fragment
android:id="#+id/FragmentB"
android:name="com.example.FragmentB"
android:label="FragmentB"
tools:layout="#layout/FragmentA">
<action
android:id="#+id/action_FragmentB_to_FragmentA"
app:destination="#id/FragmentA" />
</fragment>
Like you want to navigate with some arguments back to the FragmentA from FragmentB, use this:
findNavController().navigate(
R.id.action_FragmentB_to_FragmentA,
bundleof("key" to <your_value>) // example: 100 (Int value)
)
Then you can just receive the argument in your FragmentA using arguments?.getInt("key") or if you pass String can get it with arguments?.getString("str_key") etc.
I am using the new AndroidX navigation framework.
I have a few fragments all linked in a navigation chain.
FragmentA --> FragmentB --> FragmentC
FragmentC has a Cancel button that should send me up all the way back to FragmentA.
Should I do the following:
on FragmentC call the method:
Navigation.findNavController(view).navigateUp();
then on FragmentB listen to some callback and using some passed parameter or argument trigger another navigateUp() function from FragmentB
or is there some method that will do the equivalent of navigateUpTwice()
What I ended up doing was
Navigation.findNavController(view).popBackStack(R.id.fragmant_a,false)
In your navigation.xml file under the action that you have created to navigate to starting fragment (FragmentA in your case) add the following
<action
....
app:popUpTo="#id/fragmentA"
app:popUpToInclusive="false"/>
This will pop up all the fragments until FragmentA and will exclude FragmentA since popUpToInclusive is set to false.
Edit:
The full form of your action under FragmentC tag of your navigation.xml file will be something similar to this:
<fragment
android:id="#+id/FragmentC"
android:name="com.yourdomain.FragmentC"
android:label="FragmentC">
<action
android:id="#+id/action_fragmentC_to_fragmentA"
app:destination="#id/fragmentA"
app:popUpTo="#id/fragmentA"
app:popUpToInclusive="false"/>
</fragment>
Try with this, it works for me.
findNavController().navigateUp()
findNavController().navigateUp()
You can set pop To FragmentA in your action from FragmentB --> FragmentC and when then you press back it goes to Fragment A instead of Fragment B
So I found myself in a situation where I had to navigateUp() twice, but with a twist: I could reach the view through several paths. Think of it like this:
Fragment A --> Fragment C --> Fragment D
Fragment B --> Fragment C --> Fragment D
In a normal situation, using the popTo xml attribute or the popBackStack() method would work. However it is unusable here. Fortunately, what you can do is:
val navController = NavHostFragment.findNavController(this)
navController.navigateUp()
navController.navigateUp
As other people have pointed out, this is absolutely not optimised performance-wise. In any situation where you can reach Fragment D through a single path, use popTo instead.