11

The documentation https://developer.android.com/topic/libraries/architecture/viewmodel#sharing describes how we can share the same ViewModel across the different Fragments.

I have some complicated pages in my single Activity app with a container and tabs fragments. Each such page has own ViewModel which should be shared with all contained fragments.

The key trick here is to use Activity instead of Fragment to hold my ViewModel.

The problem is that my Activity can have multiple pages with own models and holding the view model for particular page all the time is waste of device resources.

Is there any way to control the life-cycle of ViewModel to destroy it when user leaves the page?

I thought to use the container fragment instead of Activity:

model = ViewModelProviders.of(getPageContainerFragment()).get(SharedViewModel.class);

But found this idea not so good because all children fragments should know about the parent which could be not so good.

Is there any alternatives to tackle properly such case?

engilyin
  • 1,011
  • 9
  • 22
  • Activity has multiple pages, how are you displaying those pages - fragment? – Arnav Rao Nov 10 '18 at 06:41
  • 1
    Activity with multiple page fragments. Actually just like encouraged by Navigation component of JetPack. In my case with Drawer side menu. But the challenge is that some of my fragments could have own child fragments. E.g. Tab fragment with its child pages. So I need to share the ViewModel with all one page components(fragments) – engilyin Nov 10 '18 at 17:11
  • In your case, you have single Activity, containing Fragments, which then contain shared Fragments? Could you not scope the `ViewModel` to the lifecycle of the parent Fragment instead of the Activity? – Richard Le Mesurier Dec 06 '18 at 14:06
  • I'm sorry but I'm not sure I get you. What do you mean? Something that I described in my question like: model = ViewModelProviders.of(getPageContainerFragment()).get(SharedViewModel.class); ? – engilyin Dec 06 '18 at 20:06
  • 1
    Yes, sorry, I missed that you had said exactly that. I think that's the recommended way. In [my related question](https://stackoverflow.com/a/53713645/383414) you can see someone suggesting how to manually clear the ViewModels shared in an Activity, which was your initial complaint about using up memory (mine too, which is how I found your post). – Richard Le Mesurier Dec 11 '18 at 05:28
  • 1
    I can't see any better way. I just used it like: ```ViewModelProviders.of(parentFragment!!, viewModelFactory)``` – Carson Holzheimer Dec 15 '18 at 08:14
  • there is a problem with using `ViewModelProviders.of(parentFragment!!, viewModelFactory)` coz you never know who the `parentFragment` is! If the the fragment is a child fragment of a child fragment then scoping your `ViewModel` with the `parentFragment` will create a new `ViewModel` instead. – Archie G. Quiñones Feb 26 '19 at 03:28

2 Answers2

2

If I get it right, your question is "how to free up resources" not "how to clear viewmodel".
So, you can make your viewmodels as light as possible, like this:

abstract class MyViewModel: ViewModel() {
    abstract fun freeResources()
}

and call vm.freeResources() in your OnPageChangeListener or OnTabSelectedListener or whichever listener you use, when page is changed.
In this case your should obtain viewModel using activity scope.

Alternatively, if you really want your viewmodel to be onCleared() and then the new one created, I can suggest using scoped-vm library. It allows your to request viewmodels for a scope identified by a string name.

ScopedViewModelProviders
     .forScope(fragment, "scope")
     .of(activity)
     .get(MyViewModel::class.java)

Scope gets cleared (so are viewmodels in it) as soon as last fragment that requested something from that scope gets destroyed. So use different scopes for your pages.
But, in this case you should double-check the lifecycle of your fragments: if your PagerAdapter holds them for re-use, the scope will never be cleared, and only manual approach will help you.

dhabensky
  • 1,045
  • 8
  • 15
  • Looks like you did not get my question. It was about JetPack and standard ways to manage shared ViewModels on multiple fragments. There are many ways to workaround it. E.g. do not use JetPack at all :-) The first suggestion is not the case at all. It keeps the ViewModel in the Activity anyway. For my understanding the general idea of libraries and frameworks like JetPack to free a developer from such tasks like watching for the resources, when and how to release it. It looks like at this time it is simple flaw of design. – engilyin Apr 13 '19 at 19:44
2

Since you are using Android Jetpack, I can assume that you also use Navigation Component.

If you want a ViewModel to only stay active when you are in certain fragments, you can create a navigation chart for those fragments, so that the shared ViewModel only lives while you are browsing between those fragments and is destroyed when you leave them.

Imagine that your app has these fragments,

  • VehicleFragment: inside this fragment you have tabs(SedanFragment, PickupFragment, OffroadFragment, etc.)
  • UserProfileFragment
  • LoginFragment
  • etc.

And you want to keep a ViewModel alive while you are browsing between Fragment Vehicles and its different tabs.

Well, create a nested navigation chart for them like this.

<?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/main_navigation.xml"
    app:startDestination="@id/MainFragment">

    <fragment
        android:id="@+id/MainFragment"
        android:name="com.fortatic.apps.guesstheword.ui.welcome.MainFragment"
        android:label="MainFragment"
        tools:layout="@layout/fragment_main">
        <action
            android:id="@+id/action_mainFragment_to_vehicleGraph"
            app:destination="@id/vehicleGraph" />
    </fragment>

    <navigation
        android:id="@+id/vehicleGraph"
        app:startDestination="@id/vehicleFragment" >
        <fragment
            android:id="@+id/vehicleFragment"
            android:name="com.fortatic.apps.guesstheword.ui.game.VehicleFragment"
            android:label="VehicleFragment"
            tools:layout="@layout/fragment_vehicle">
            <action
                android:id="@+id/action_fragmentVehicle_to_sedanFragment"
                app:destination="@id/sedanFragment"/>
            <action
                android:id="@+id/action_fragmentVehicle_to_pickupsFragment"
                app:destination="@id/pickupsFragment"/>
            <action
                android:id="@+id/action_fragmentVehicle_to_offroadFragment"
                app:destination="@id/offroadFragment"/>        
        </fragment>
        <fragment
            android:id="@+id/sedanFragment"
            android:name="com.fortatic.apps.guesstheword.ui.score.SedanFragment"
            android:label="SedanFragment"
            tools:layout="@layout/fragment_sedan">
            ...
        </fragment>
        <fragment
            android:id="@+id/pickupsFragment"
            android:name="com.fortatic.apps.guesstheword.ui.score.PickupFragment"
            android:label="PickupFragment"
            tools:layout="@layout/fragment_pickups">
            ...
        </fragment>
        <fragment
            android:id="@+id/offroadFragment"
            android:name="com.fortatic.apps.guesstheword.ui.score.OffroadFragment"
            android:label="OffroadFragment"
            tools:layout="@layout/fragment_offroad">
            ...
        </fragment>
    </navigation>
</navigation>

Once you have created the nested navigation graph, simply request an instance of ViewModel using:

private val mySharedViewModel: SharedViewModel by navGraphViewModels(R.id.myNestedGraph) {
    //defaultViewModelProviderFactory or the ViewModelProvider.Factory you are using.
}

You can find more details in this answer

Alejandro Fort
  • 545
  • 4
  • 9