Get parent fragment from child when using Navigation Component - android

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.

Related

Pass data back to previous fragment using Android Navigation

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

Get fragment from NavController

I have one NavController (navOrdersController) with 2 fragments inside. My current fragment is OrderDetailFragment and I want access to first Fragment (OrdersFragment) to update data in MainActivity. How can I do this?
I tried findFragmentByTag and findFragmentByID but always return null.
I got access OrderDetailFragment with:
this.supportFragmentManager.fragments[position].findNavController().currentDestination
But I need previous fragment. Thank you very much
Can't say the code snippet (I've added below) is the best solution, but it worked for me to get the recent/current fragment added in NavHostFragment used in Android Navigation Component's implementations
OR
You can even override onActivityResult of the you activity with your fragment's, all you need is to get the added Fragment and call it's onActivityResult, using this code snippet:
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
Fragment fragment = navHostFragment.getChildFragmentManager().getFragments().get(0);
fragment.onActivityResult(requestCode, resultCode, data);
If both the fragments have same parent activity, i.e. use getActivity() to get activity reference and use Activity.getSupportFragmentManager to find the fragment.
.....
Activity X -> Fragment A -> Fragment B
If fragment B is child of Fragment A, use getParentFragment. The fragment you are looking for would be this fragment.
Let me know if anything is not clear.
PS: Jetpack Navigation library does the same thing which we have been doing explicit. So, the previous logic of Activity-Fragment is still valid.
EDIT:
try out (parentFragment as NavHostFragment).childFragmentManager.findFragmentById()
I forgot last time that activity has declaration of NavHostFragment in its layout so there will always root parent fragment of type 'NavHostFrament'
EDIT:
I have found a work around for it
parentFragment.childFragmentManager.getFragment(Bundle().also { it.putInt("foobar",0) }, "foobar")

Android ViewModel Sharing Between Fragments [duplicate]

I'm using the navigation component, I want a view model to be shared between a few fragments but they should be cleared when I leave the fragments (hence not scoping them to the activity) I'm trying to take the one activity many fragments approach. I have managed to achieve this using multiple nav hosts and scoping the fragments to it using getParentFragment but this just leads to more issues having to wrap fragments in other parent fragments, losing the back button working seamlessly and other hacks to get something to work that should be quite simple. Does anyone have a good idea on how to achieve this? I wondered if theres anything with getViewModelStore I could be using, given the image below I want to scope a view model to createCardFragment2 and use it in anything after it (addPredictions, editImageFragment, and others i haven't added yet), but then if I navigate back to mainFragment I want to clear the view models.
BTW I cant just call clear on mainFragment view model store as there are other view models here that shouldn't be cleared, I guess i want a way to tell the nav host what the parent fragment should be which I'm aware isn't going to be a thing, or a way to make the view model new if I'm navigating from mainFragment or cardPreviewFragment
Here's a concrete example of Alex H's accepted answer.
In your build.gradle (app)
dependencies {
def nav_version = "2.1.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
}
Example of view model
class MyViewModel : ViewModel() {
val name: MutableLiveData<String> = MutableLiveData()
}
In your FirstFlowFragment.kt define
val myViewModel: MyViewModel by navGraphViewModels(R.id.your_nested_nav_id)
myViewModel.name.value = "Cool Name"
And in your SecondFlowFragment.kt define
val myViewModel: MyViewModel by navGraphViewModels(R.id.your_nested_nav_id)
val name = myViewModel.name.value.orEmpty()
Log.d("tag", "welcome $name!")
Now the ViewModel is scoped in this nested fragment, shared state will be destroyed when nested nav is destroyed as well, no need to manually reset them.
Yes, it's possible to scope a viewmodel to a navgraph now starting with androidx.navigation:*:2.1.0-alpha02. See the release notes here and an example of the API here. All you need to give is the R.id for your navgraph. I find it a bit annoying to use, though, because normally viewmodels are initialized in onCreate, which isn't possible with this scope because the nav controller isn't guaranteed to be set by your nav host fragment yet (I'm finding this is the case with configuration changes).
Also, if you don't want your mainFragment to be part of that scope, I would suggest taking it out and maybe using a nested nav graph.
so when i posted this the functionality was there but didn't quite work as expected, since then i now use this all the time and this question keeps getting more attention so thought i would post an up to date example,
using
//Navigation
implementation "androidx.navigation:navigation-fragment:2.2.0-rc04"
// Navigation UI
implementation "androidx.navigation:navigation-ui:2.2.0-rc04"
i get the view model store owner like this
private ViewModelStoreOwner getStoreOwner() {
NavController navController = Navigation
.findNavController(requireActivity(), R.id.root_navigator_fragment);
return navController.getViewModelStoreOwner(R.id.root_navigator);
}
im using the one activity multiple fragments implementation, but using this i can effectively tie my view models to just the scoped fragments and with the new live data you can even limit that too
the first id comes from the nav graphs fragment
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<fragment
android:id="#+id/root_navigator_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="#navigation/root_navigator"/>
</FrameLayout>
and the second comes from the id of the nav 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/root_navigator"
app:startDestination="#id/mainNavFragment">
and then you can use it like so
private void setUpSearchViewModel() {
searchViewModel = new ViewModelProvider(getStoreOwner()).get(SearchViewModel.class);
}
So based on the answers here I made a function that lazily returns a ViewModel scoped to the current navigation graph.
private val scopedViewModel by lazy { getNavScopedViewModel(arg) }
/**
* The [navGraphViewModels] function is not entirely lazy, as we need to pass the graph id
* immediately, but we cannot call [findNavController] to get the graph id from, before the
* Fragment's [onCreate] has been called. That's why we wrap the call in a function and call it lazily.
*/
fun getNavScopedViewModel(arg: SomeArg): ScopedViewModel {
// The id of the parent graph. If you're currently in a destination within this graph
// it will always return the same id
val parentGraphScopeId = findNavController().currentDestination?.parent?.id
?: throw IllegalStateException("Navigation controller should already be initialized.")
val viewModel by navGraphViewModels<ScopedViewModel>(parentGraphScopeId) {
ScopedViewModelFactory(args)
}
return viewModel
}
It's not the prettiest implementation but it gets the job done

Navigation Component .popBackStack() with arguments

I have Two fragment. SecondFragment and ThirdFragment. Actually I use the Navigation Component for passing value between fragments. Like this:
SecondFragment:
val action = SecondFragmentDirections.action_secondFragment_to_thirdFragment().setValue(1)
Navigation.findNavController(it).navigate(action)
Here is how I read the value from the ThirdFragment:
arguments?.let {
val args = ThirdFragmentArgs.fromBundle(it)
thirdTextView.text = args.value.toString()
}
It's work fine. Now my stack is look like this:
ThirdFragment
SecondFragment
There is any option for pass value from the opened ThirdFragment to the previous SecondFragment with the new Navigation Component? (When ThirdFragment is finishing)
I know about onActivityResult, but If Nav.Component serve better solution than I want use that.
Thank you!
It's a bit late for this answer but someone may find it useful. In the updated versions of the navigation component library it is now possible to pass data while navigating back.
Suppose the stack is like this
FragmentA --> FragmentB.
We are currently now in FragmentB and we want to pass data when we go back to FragmentA.
Inside FragmentAwe can create an observer with a key:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController()
// Instead of String any types of data can be used
navController.currentBackStackEntry?.savedStateHandle?.getLiveData<String>("key")
?.observe(viewLifecycleOwner) {
}
}
Then inside FragmentB if we change its value by accessing previous back stack entry it will be propagated to FragmentA and observer will be notified.
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("key", "value that needs to be passed")
navController.popBackStack()
Just came across setFragmentResult(), pretty easy to use. The docs on this are here.
If you are navigating: Fragment A -> Fragment B -> Fragment A
Add this to fragment A:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFragmentResultListener("requestKey") { requestKey, bundle ->
shouldUpdate = bundle.getBoolean("bundleKey")
}
}
Then in fragment B add this line of code:
setFragmentResult("requestKey", bundleOf("bundleKey" to "value to pass back"))
// navigate back toFragment A
When you navigate back to fragment A the listener will trigger and you'll be able to get the data in the bundle out.
What you are asking for is an anti-pattern. You should either
navigate to the second fragment again with the new values you would like to set
use the third fragment ins a separate activity and start it with startActivityForResult()
use a ViewModel or some kind of singleton pattern to hold on to your data (make sure you clear the data after you no longer need it)
these are some of the patterns that came to my mind. Hope it helps.
As described here:
When navigating using an action, you can optionally pop additional destinations off of the back stack. For example, if your app has an initial login flow, once a user has logged in, you should pop all of the login-related destinations off of the back stack so that the Back button doesn't take users back into the login flow.
To pop destinations when navigating from one destination to another, add an app:popUpTo attribute to the associated element. app:popUpTo tells the Navigation library to pop some destinations off of the back stack as part of the call to navigate(). The attribute value is the ID of the most recent destination that should remain on the stack.
<fragment
android:id="#+id/c"
android:name="com.example.myapplication.C"
android:label="fragment_c"
tools:layout="#layout/fragment_c">
<action
android:id="#+id/action_c_to_a"
app:destination="#id/a"
app:popUpTo="#+id/a"
app:popUpToInclusive="true"/>
</fragment>

How can I get Fragment from View?

I added some Fragment into a TableLayout and I want to manage them from my container Activity, so I used this:
Fragment fragment = (Fragment) tableLayout.getChildAt(i);
but getChildAt(int) returns a View and a View could NOT cast to Fragment
I don't understand why people are down-voting your question. Fragments can be very confusing at times, especially for beginners. To understand your problem, you must learn what is a Fragment and how they are used.
To start with, a View is something that has an existence on the screen. Examples include: TextView, EditText, Button, etc. They are placed inside "layouts" written in Xml or Java/Kotlin. These layouts are shown using an Activity.
Now, a Fragment is not a View. It does not have any existence on the screen at all. Instead, it's a class that simply manages a "layout" — kinda similar to an Activity. If you need the View returned by your Fragment's onCreateView(), you can directly use findViewById() within your Activity.
If you need a reference to your Fragment, there are two possible ways of doing this:
1) If you added the Fragment programmatically like this
getFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container_viewgroup, myFragment, FRAGMENT_TAG)
.commit();
You can use:
MyFragment myFragment = (MyFragment) getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
2) If you added the Fragment inside an XML layout like this:
<fragment android:name="com.example.android.fragments.HeadlinesFragment"
android:id="#+id/fragmentContainer"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent" />
You can use this:
getFragmentManager().findFragmentById(R.id.fragmentContainer);
Basically, each Activity has a FragmentManager class that maintains all the active Fragments, and there are two ways of finding them: Using a unique TAG that you pass while showing a fragment, or passing the container view-ID where the fragment was added.
For people looking how to actually get a reference to the Fragment object from a View there is now a method in FragmentManager called findFragment(View) (reference)
//in Java
FragmentManager.findFragment(view)
//in Kotlin there is an extension function
view.findFragment()
Be careful - it will throw an IllegalStateException if the view was not added via a fragments onCreateView.
You can not get a fragment like this. You will have to add fragment with a tag and retrieve it by that tag.
to add a fragment do following:
getFragmentManager().beginTransaction().add(R.id.container, fragment, "tagTofindFragment");
to get fragment:
fragment = getFragmentManager().findFragmentByTag("tagTofindFragment");
Here tagTofindFragment is that tag that should be unique among your fragments.

Categories

Resources