26

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

hushed_voice
  • 3,161
  • 3
  • 34
  • 66
martinseal1987
  • 1,862
  • 8
  • 44
  • 77
  • 1
    You can refer to this answer https://stackoverflow.com/a/52732831/10271334, let me know if any confusion. – Jeel Vankhede Dec 17 '18 at 14:24
  • ok so this is what i was already trying let me add some code – martinseal1987 Dec 17 '18 at 14:55
  • 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. – cincy_anddeveloper Dec 17 '18 at 15:03
  • @WadeWilson I've added what I'm doing in my code the initialize the view models but the data isnt being shared between the two – martinseal1987 Dec 17 '18 at 15:05
  • 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. – cincy_anddeveloper Dec 17 '18 at 15:05
  • @WadeWilson no I'd like the data between them to be the same, so sharing the same instance of the view model – martinseal1987 Dec 17 '18 at 15:06
  • 1
    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. – cincy_anddeveloper Dec 17 '18 at 15:08
  • @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 same – martinseal1987 Dec 17 '18 at 15:19
  • 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 it – martinseal1987 Dec 17 '18 at 15:24
  • 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: https://developer.android.com/topic/libraries/architecture/viewmodel#sharing – cincy_anddeveloper Dec 17 '18 at 15:35
  • 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 anymore – martinseal1987 Dec 17 '18 at 15:40
  • One question @martin : Have you added/replaced your child fragments using **Child Fragment Manager** or just by **Fragment Manager** from Parent Fragment ? – Jeel Vankhede Dec 17 '18 at 15:43
  • @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 fragment – martinseal1987 Dec 17 '18 at 15:45
  • 1
    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. – Jeel Vankhede Dec 17 '18 at 15:50
  • @JeelVankhede they have the same ID D/RSRQSTFRGMNT: parent fragment id 2131362099 D/RqstCatFrag: child fragment id 2131362099 – martinseal1987 Dec 17 '18 at 15:56
  • @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 accept – martinseal1987 Dec 17 '18 at 16:33

9 Answers9

51

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() }
)
Alok Mishra
  • 1,904
  • 1
  • 17
  • 18
  • 4
    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 approach – martinseal1987 Jul 23 '20 at 07:20
  • 1
    @martinseal1987 I have updated changes as per your request. viewModels import androidx.fragment.app.viewModels which is part of Fragment-ktx . Thanks – Alok Mishra Jul 23 '20 at 19:06
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

martinseal1987
  • 1,862
  • 8
  • 44
  • 77
  • 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. ` – Bagus Aji Santoso Aug 09 '20 at 15:36
  • use viewLifecycleOwner, pokemonListViewModel.searchPokemon.observe(viewLifecycleOwner, Observer { pokemonList -> – martinseal1987 Aug 09 '20 at 18:08
  • 1
    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. – dumbfingers Dec 22 '20 at 08:59
11

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()})
Ndriçim Sadiku
  • 151
  • 1
  • 5
  • 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? – niall8s Sep 29 '21 at 10:34
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 !
Jeel Vankhede
  • 11,592
  • 2
  • 28
  • 58
  • 1
    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 again – martinseal1987 Dec 18 '18 at 15:13
  • 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. – Jeel Vankhede Dec 18 '18 at 15:25
  • you can fix the back button by calling getActivity().getSupportFragmentManager().popBackStack() – martinseal1987 Dec 19 '18 at 14:45
  • 1
    Wouldn't `getParentFragment()` return null if its the parent fragment. In that case, that method will throw an exception? – Archie G. Quiñones Feb 21 '19 at 07:59
  • No, **object** will simply be **null**. This piece of code won't be throwing any exception. – Jeel Vankhede Feb 21 '19 at 10:17
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.

hushed_voice
  • 3,161
  • 3
  • 34
  • 66
  • 1
    completely agree i asked another question that warrants the same answer here https://stackoverflow.com/questions/56505455/scoping-a-viewmodel-to-multiple-fragments-not-activity-using-the-navigation-co – martinseal1987 May 21 '20 at 09:32
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.

Peter Z.
  • 159
  • 1
  • 5
2

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()
Abdul Wahab
  • 411
  • 6
  • 15
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() })
Stachu
  • 1,614
  • 1
  • 5
  • 17
  • It works even when you don't pass `this` in ParentFragment. `this` is passed by default in viewModels(). Btw thanks it worked well. – Pratik Saluja May 24 '23 at 09:36
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