15

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.

Ziens
  • 381
  • 2
  • 8
  • can you post the code for adding the child fragment – Fahad Alotaibi Mar 01 '20 at 14:08
  • @FahadAlotaibi, simply findNavController().navigate(R.id.start_game_fragment) – Ziens Mar 01 '20 at 14:57
  • so, NavHostFragment will be the parent fragment and all the fragments will be added as child fragment – Fahad Alotaibi Mar 01 '20 at 15:09
  • @FahadAlotaibi, yes, this NavHostFragment is common to all fragments. It is instantiated in activity. It will live while activity is alive. – Ziens Mar 01 '20 at 15:18
  • @FahadAlotaibi, For example, if I use the approach one Fragment, one ViewModel, when the fragment ends, the ViewModel will call onCleared () – Ziens Mar 01 '20 at 15:21
  • @Ziens also have a look at my questions https://stackoverflow.com/questions/70721218/receive-fragment-result-in-navigation-component-fragment and https://stackoverflow.com/questions/70680327/toolbar-icon-handling-using-navigation-component – Taimoor Khan Jan 16 '22 at 07:24

4 Answers4

4

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

Ziens
  • 381
  • 2
  • 8
2

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

Hari Kiran
  • 188
  • 13
1

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.

1

Use this piece of code to access parent fragment functions using navigation component:

NavHostFragment navHostFragment = (NavHostFragment) getParentFragment();
ParentFragment parent = (ParentFragment) navHostFragment.getParentFragment();
parent.somePublicFunctin();
Ghazal
  • 123
  • 2
  • 9