How to scope a view model to a parent fragment?
Asked Answered
W

9

29

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

Williemaewillies answered 17/12, 2018 at 14:14 Comment(16)
You can refer to this answer https://mcmap.net/q/464812/-viewmodel-for-fragment-instead-accessing-activity-viewmodel, let me know if any confusion.Thimerosal
ok so this is what i was already trying let me add some codeWilliemaewillies
I'm not sure if I am missing something but you can totally register a ViewModel with a particular fragment instance, so you wouldn't have to worry about it getting returned for a fragment.Geminian
@WadeWilson I've added what I'm doing in my code the initialize the view models but the data isnt being shared between the twoWilliemaewillies
After reading your revision, if you want the two fragments, parent and child fragment to have their own RequestViewModel instance that is unique to them, don't call getParentFragment() in the child fragment. if you want to share the viewmodel, the object reference you pass in must be identical. I think you may need to make sure that you're passing the same instance into the .of(...) call.Geminian
@WadeWilson no I'd like the data between them to be the same, so sharing the same instance of the view modelWilliemaewillies
If you want to share the Viewmodel, the object reference you pass in must be identical. I think you may need to make sure that you're passing the same instance into the .of(...) call. Either debug, or modify the toString method to return a unique value and log out the value you're passing in to ViewModelProviders.of(...) inside both the parent and child Fragment.Geminian
@WadeWilson so i did what you suggested and got these RaiseRequestFragment{9afdd34 #1 id=0x7f0a0133} NavHostFragment{1333143 #0 id=0x7f0a0133} so the ids are the sameWilliemaewillies
nav host isnt the fragment that calls it but it is the fragment that holds my navigation graph so i guess it is the parent but the id is from the fragment that calls itWilliemaewillies
I'm not sure what you did is the same thing because I cannot see your code. The fragment instance passed into ViewModelsProvider.of(...) must be identical in both the parent and child fragment. I know you can share ViewModels instances between fragments by passing in a reference to the same parent activity. Have you tried doing that? View this link for more details: developer.android.com/topic/libraries/architecture/…Geminian
I was previously using the activity to achieve it and yes that worked fine after trying to change this to the fragment scope it didn't work anymoreWilliemaewillies
One question @martin : Have you added/replaced your child fragments using Child Fragment Manager or just by Fragment Manager from Parent Fragment ?Thimerosal
@JeelVankhede neither I'm using the new navigation architecture component, which I'm guessing will use the fragment manager when calling from activities and the child fragment manager when calling from a fragmentWilliemaewillies
Can you try to print or debug hash code for your shared ViewModels in parent & child fragment just to verify that they're shared? If they're both same meaning they're shared.. also check for your child of child fragments. It may be cause.Thimerosal
@JeelVankhede they have the same ID D/RSRQSTFRGMNT: parent fragment id 2131362099 D/RqstCatFrag: child fragment id 2131362099Williemaewillies
@JeelVankhede I've added my answer as i found what it was but is essentially your answer if you want to add it i'll be happy to acceptWilliemaewillies
W
13

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

Williemaewillies answered 17/12, 2018 at 16:32 Comment(3)
On the observe part, what do we send as the lifecycle owner? I got Unsafe call to observe with Fragment instance as LifecycleOwner from MyFragment.onViewCreated. Camiecamila
use viewLifecycleOwner, pokemonListViewModel.searchPokemon.observe(viewLifecycleOwner, Observer { pokemonList ->Williemaewillies
So I am using the scoped viewModel method: by viewModels to lazily create viewModels. And in order to share the same viewModel between fragment and the bottomSheetFragmentDialog it launched, i need to use `by viewModels({ requireParentFragment() }) in both fragment and bottomSheetFragment.Cheltenham
B
57

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() }
)
Bisitun answered 23/7, 2020 at 7:0 Comment(2)
theres currently a bug with this when tying hilt to the navigation component you need to specify the default view model factory, otherwise this will be the defacto approachWilliemaewillies
@Williemaewillies I have updated changes as per your request. viewModels import androidx.fragment.app.viewModels which is part of Fragment-ktx . ThanksBisitun
W
13

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

Williemaewillies answered 17/12, 2018 at 16:32 Comment(3)
On the observe part, what do we send as the lifecycle owner? I got Unsafe call to observe with Fragment instance as LifecycleOwner from MyFragment.onViewCreated. Camiecamila
use viewLifecycleOwner, pokemonListViewModel.searchPokemon.observe(viewLifecycleOwner, Observer { pokemonList ->Williemaewillies
So I am using the scoped viewModel method: by viewModels to lazily create viewModels. And in order to share the same viewModel between fragment and the bottomSheetFragmentDialog it launched, i need to use `by viewModels({ requireParentFragment() }) in both fragment and bottomSheetFragment.Cheltenham
F
12

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()})
Fuegian answered 6/1, 2021 at 13:55 Comment(1)
A little late to the party here but I'm wondering if you were able to test this with fragmentscenario? My tests fail because the fragment under test is not a child and directly attached to EmptyFragmentActivity for tests. I could fake two parent fragments to work around but there must be a nicer way?Anabolism
T
8

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 !
Thimerosal answered 17/12, 2018 at 17:11 Comment(5)
there is more to this if you declare the fragment as a fragment in xml and supply it a graph in xml then that fragment will be the parent fragment meaning i can seperate some of my navigation flow, but unfortunately this will sometimes break your popBackStack calls because you end up needing a container to hold the fragment, i HATE the navigation component! as it is i recommend not using as i believe it to be broken but my company wants me to use it as it should be the defacto way going forward, thanks againWilliemaewillies
Yes, actually it's recommended that if any library or dependency is in alpha then avoid or try not to use it until it's stable; for the sake of final stable application.Thimerosal
you can fix the back button by calling getActivity().getSupportFragmentManager().popBackStack()Williemaewillies
Wouldn't getParentFragment() return null if its the parent fragment. In that case, that method will throw an exception?Genevivegenevra
No, object will simply be null. This piece of code won't be throwing any exception.Thimerosal
A
5

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.

Ardolino answered 21/5, 2020 at 7:48 Comment(1)
completely agree i asked another question that warrants the same answer here #56505955Williemaewillies
F
4

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.

Floro answered 23/3, 2021 at 18:47 Comment(0)
C
3

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()
Chimerical answered 28/1, 2021 at 5:52 Comment(0)
F
1

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() })
Fipple answered 28/12, 2020 at 11:59 Comment(1)
It works even when you don't pass this in ParentFragment. this is passed by default in viewModels(). Btw thanks it worked well.Magyar
A
0

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

Adriene answered 14/4, 2021 at 19:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.