How to scope a view model to a parent fragment?

Viewed 21012

So I'm using the new navigation component (with the one activity principle) and communicating between each fragment using shared view models, however, I've come to a point where I sometimes need to clear the view model but I can't find a good place to clear it. But tbh I think rather than trying to clear it myself, I should really be allowing the framework to do it for me, but it doesn't because the view models are shared and scoped to the activity, but I think I can scope them to a parent fragment, I made a drawing to illustrate what I'm trying to do.my navigation flow so I only want to clear the 2 view models when I navigate back from "Child 1 Child a" currently the view models are never cleared, trying to get the view model currently by calling 'this' in the fragment and getParentFragment in the child doesn't work, can anyone provide an example?

EDIT

It looks like I was already doing something similar but it's not working in my case so I will add some code, here is how I access the first view model in the parent fragment

model = ViewModelProviders.of(this).get(RequestViewModel.class);

and then in the child fragment, I'm doing this

requestViewModel = ViewModelProviders.of(getParentFragment()).get(RequestViewModel.class);

but it's not keeping the data between them, they both have observers attached

9 Answers

Using Fragment-ktx libr in your app you can get viewModel as below in your app-> build.gradle

 implementation 'androidx.fragment:fragment-ktx:1.1.0'

// get ViewModel in ParentFragment as

 private val viewModel: DemoViewModel by viewModels()

// get same instance of ViewModel in ChildFragment as

 private val viewModel: DemoViewModel by viewModels(
    ownerProducer = { requireParentFragment() }
)

Ok so using this in the parent

model = ViewModelProviders.of(this).get(RequestViewModel.class);

and this in the child

requestViewModel = ViewModelProviders.of(getParentFragment()).get(RequestViewModel.class);

were giving me different hashcodes but the same IDs and it seems to be because of the navigation component, if i change them both to getParentFragment then it works, so i think the component is replacing fragments instead of adding them here, many thanks to @WadeWilson and @JeelVankhede

So, as per @martin's proposed solution derives that even if One/Many Fragments added as Child inside Parent Fragment, Navigation component provides same Fragment manager to both fragments.

Meaning that even if fragments are added as parent-child hierarchy, they'll share same Fragment manager from Navigation component (might be bug in this library ?) & so that ViewModels are not shared due to this dilemma when using getParentFragment() instance for ViewModelProvider inside child fragment.


So, one quick solution to share ViewModels would be getting instance of Parent fragment from Fragment manager using below line for both parent & child fragments :

ViewModelProviders.of(getParentFragment()).get(SharedViewModel.class); // using this in both parent amd child fragment would do the trick !

If you are using Navigation Component (https://developer.android.com/guide/navigation), you need to get the viewModel this way:

Implement fragment-ktx in your app -> build.gradle:

implementation 'androidx.fragment:fragment-ktx:1.2.5

In parent fragment:

private val viewModel by viewModels<ParentFragmentViewModel>()

In child fragment

private val viewModel by viewModels<ParentFragmentViewModel>({requireGrandParentFragment()})

the requireGrandParentFragment() is a custom extension of Fragment:

fun Fragment.requireGrandParentFragment() = this.requireParentFragment().requireParentFragment()

The reason you need to go two levels up to access the viewModel of the parentFragment is because first parent of the childFragment on navigation component is the NavHostFragment and the parent of NavHostFragment is the parentFragment where the viewModel is.

If you are not using Navigation component you can access it like this in childFragment:

private val viewModel by viewModels<ParentFragmentViewModel>({requireParentFragment()})

Google has given us the ability to scope ViewModel to navigation graphs now. You can use it if you are using the navigation component already. (Personal opinion, you SHOULD move to navigation component if you are not already using it, as it is soo easy to use. Take it from a guy who tried to manage the back stack myself)

You can select all the fragments that need to be grouped together inside the nav graph and right-click->move to nested graph->new graph

now this will move the selected fragments to a nested graph inside the main nav graph like this:

<navigation app:startDestination="@id/homeFragment" ...>
    <fragment android:id="@+id/homeFragment" .../>
    <fragment android:id="@+id/productListFragment" .../>
    <fragment android:id="@+id/productFragment" .../>
    <fragment android:id="@+id/bargainFragment" .../>
    
    <navigation 
        android:id="@+id/checkout_graph" 
        app:startDestination="@id/cartFragment">

        <fragment android:id="@+id/orderSummaryFragment".../>
        <fragment android:id="@+id/addressFragment" .../>
        <fragment android:id="@+id/paymentFragment" .../>
        <fragment android:id="@+id/cartFragment" .../>

    </navigation>
    
</navigation>

Now, inside the fragments when you initialize the ViewModel do this

val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)

If you need to pass the viewmodel factory(may be for injecting the viewmodel) you can do it like this:

val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph) { viewModelFactory }

Make sure its R.id.graph and not R.navigation.graph

For some reason creating the nav graph and using include to nest it inside the main nav graph was not working for me. Probably it is a bug.

Source: https://medium.com/androiddevelopers/viewmodels-with-saved-state-jetpack-navigation-data-binding-and-coroutines-df476b78144e

Thanks, Manually clearing an Android ViewModel? for pointing me in the right direction.

I have got a same problem tried above all solution but not working in my scenario come up with a other solution using requireParentFragment() return NavHostFragment in child fragment solve it by using

private val viewModel: childViewModel by viewModels(
            ownerProducer = { requireParentFragment().childFragmentManager.primaryNavigationFragment!! }
    )

in Parent Fragmet use this

private val viewModel: MyOrdersVM by viewModels()

What I was doing wrong was providing incorrect fragment manager to DialogFragment. So it works this way in my case:

class MyDialog : DialogFragment() {

    private val viewModel: MyViewModel by viewModels({ requireParentFragment() })

And initialization of the dialog in my fragment:

private fun showDialog(){
    MyDialog().show(childFragmentManager, "AddFriendToGroupDialog")
}

And I am using implementation 'androidx.fragment:fragment-ktx:1.3.0' and Navigation graphs.

version using Kotlin and lazy initialization (AKA by viewModels)

in the parent Fragment (e.g. ViewPager2)

private val viewModel: MyViewModel by viewModels({ this }) {
    TODO("MyViewModelFactory instance")
}

in the child Fragment (e.g. page in the ViewPager2)

private val viewModel: MyViewModel by viewModels({ requireParentFragment() })

I had this problem too. A new ViewModel was created for the child fragment. The problem is that getParentFragment () returns NavHostFragment instead of the desired fragment.

And we need to get real parent object inside child fragment.

If we do this requireParentFragment().requireParentFragment(), then we get the real parent.

Solution: (for child update)

Fragment parent = requireParentFragment().requireParentFragment();
viewModel = new ViewModelProvider(parent).get(RequestViewModel.class);

Found a solution here

Related