77

Edit: This question is a bit out of date now that Google has given us the ability to scope ViewModel to navigation graphs. The better approach (rather than trying to clear activity-scoped models) would be to create specific navigation graphs for the right amount of screens, and scope to those.


With reference to the android.arch.lifecycle.ViewModel class.

ViewModel is scoped to the lifecycle of the UI component it relates to, so in a Fragment-based app, that will be the fragment lifecycle. This is a good thing.


In some cases one wants to share a ViewModel instance between multiple fragments. Specifically I am interested in the case where many screens relate to the same underlying data.

(The docs suggest similar approach when multiple related fragments are displayed on the same screen but this can be worked around by using a single host fragment as per answer below.)

This is discussed in the official ViewModel documentation:

ViewModels can also be used as a communication layer between different Fragments of an Activity. Each Fragment can acquire the ViewModel using the same key via their Activity. This allows communication between Fragments in a de-coupled fashion such that they never need to talk to the other Fragment directly.

In other words, to share information between fragments that represent different screens, the ViewModel should be scoped to the Activity lifecycle (and according to Android docs this can also be used in other shared instances).


Now in the new Jetpack Navigation pattern, it is recommended to use a "One Activity / Many Fragments" architecture. This means that the activity lives for the whole time the app is being used.

i.e. any shared ViewModel instances that are scoped to Activity lifecycle will never be cleared - the memory remains in constant use.

With a view to preserving memory and using as little as required at any point in time, it would be nice to be able to clear shared ViewModel instances when no longer required.


How can one manually clear a ViewModel from it's ViewModelStore or holder fragment?

Richard Le Mesurier
  • 27,993
  • 19
  • 127
  • 242
  • related: [Shared ViewModel lifecycle for Android JetPack](https://stackoverflow.com/questions/53236574/shared-viewmodel-lifecycle-for-android-jetpack) – Richard Le Mesurier Dec 06 '18 at 14:05
  • 1
    Hey! How about creating your own retained fragment and scoping your viewmodel to that retained fragment? Now, you have total control on the lifecycle of your viewmodel. You just need to make the activity add or remove the fragment if needed and wire the retained fragment and other fragments together through the activity. It does sounds like writing some boiler plate code though but I wanna know what you think. – Archie G. Quiñones Feb 26 '19 at 08:01
  • I have no idea if it's ok to use getTargetFragment() for scope: `ViewModelProvider(requireNotNull(targetFragment)).get(MyViewModel::class.java)` –  Oct 22 '19 at 10:07
  • Yes, there is a way to do so, I have explained it [here](https://stackoverflow.com/a/59616636/8551764) – Mostafa Arian Nejad Jan 06 '20 at 17:50
  • 1
    for people trying to implement the updated solution go here https://medium.com/androiddevelopers/viewmodels-with-saved-state-jetpack-navigation-data-binding-and-coroutines-df476b78144e – hushed_voice May 20 '20 at 18:22

12 Answers12

22

If you check the code here you'll find out, that you can get the ViewModelStore from a ViewModelStoreOwner and Fragment, FragmentActivity for example implements, that interface.

Soo from there you could just call viewModelStore.clear(), which as the documentation says:

 /**
 *  Clears internal storage and notifies ViewModels that they are no longer used.
 */
public final void clear() {
    for (ViewModel vm : mMap.values()) {
        vm.clear();
    }
    mMap.clear();
}

N.B.: This will clear all the available ViewModels for the specific LifeCycleOwner, this does not allow you to clear one specific ViewModel.

kuelye
  • 606
  • 7
  • 9
Robert Nagy
  • 2,418
  • 9
  • 26
  • Very nice, I was looking in this direction but missed the obvious part that, as you say "`FragmentActivity` ... implements, that interface [`ViewModelStoreOwner`]". – Richard Le Mesurier Dec 11 '18 at 05:17
  • 1
    Ok so we could clear the ViewModel manually but is it a good idea? If I clear the view model through this method is there anything i should look after or made sure i did correctly? – Archie G. Quiñones Feb 23 '19 at 04:33
  • I also noticed you could not clear only a specific viewmodel which should be the case. If you call viewmodelstoreowner.clear() all stored viewmodel will be cleared. – Archie G. Quiñones Feb 26 '19 at 02:59
  • 1
    A word of warning with this, if you're using the new `SavedStateViewModelFactory` to create a particular view model you will need to call `savedStateRegistry.unregisterSavedStateProvider(key)` - the key being the one you should use when you call `ViewModelProvider(~).get(key, class)`. Otherwise if you attempt to get (ie. create) the viewmodel in future you will get `IllegalArgumentException: SavedStateProvider with the given key is already registered` – hmac Nov 28 '19 at 19:16
22

Quick solution without having to use Navigation Component library:

getActivity().getViewModelStore().clear();

This will solve this problem without incorporating the Navigation Component library. It’s also a simple one line of code. It will clear out those ViewModels that are shared between Fragments via the Activity

Sakiboy
  • 6,255
  • 6
  • 47
  • 63
7

I think I have a better solution.

As stated by @Nagy Robi, you could clear the ViewModel by call viewModelStore.clear(). The problem with this is that it will clear ALL the view model scoped within this ViewModelStore. In other words, you won't have control of which ViewModel to clear.

But according to @mikehc here. We could actually create our very own ViewModelStore instead. This will allow us granular control to what scope the ViewModel have to exist.

Note: I have not seen anyone do this approach but I hope this is a valid one. This will be a really good way to control scopes in a Single Activity Application.

Please give some feedbacks on this approach. Anything will be appreciated.

Update:

Since Navigation Component v2.1.0-alpha02, ViewModels could now be scoped to a flow. The downside to this is that you have to implement Navigation Component to your project and also you have no granualar control to the scope of your ViewModel. But this seems to be a better thing.

Archie G. Quiñones
  • 5,945
  • 5
  • 32
  • 71
  • Yes you're right Archie G. I think generally speaking we shouldn't manually clear VMs, and scoping to navigation graphs offers a really nice and clean way of handling the scopes of ViewModels – Robert Nagy May 07 '20 at 05:01
  • For people trying to implement the updated solution go here: https://medium.com/androiddevelopers/viewmodels-with-saved-state-jetpack-navigation-data-binding-and-coroutines-df476b78144e – hushed_voice May 20 '20 at 18:22
6

If you don't want the ViewModel to be scoped to the Activity lifecycle, you can scope it to the parent fragment's lifecycle. So if you want to share an instance of the ViewModel with multiple fragments in a screen, you can layout the fragments such that they all share a common parent fragment. That way when you instantiate the ViewModel you can just do this:

CommonViewModel viewModel = ViewModelProviders.of(getParentFragment()).class(CommonViewModel.class);

Hopefully this helps!

AvidRP
  • 323
  • 2
  • 11
  • 1
    What you write is true, but this is for a case where I do want to scope it to the `Activity` lifecycle, specifically to share it between multiple fragments that may not be displayed at the same time. This is a good response in the other case that I mentioned and I think I must update my question to remove that case (as it creates confusion - apologies for that) – Richard Le Mesurier Dec 06 '18 at 14:17
6

As OP and Archie said, Google has given us the ability to scope ViewModel to navigation graphs. I will add how to do it here if you are using the navigation component already.

You can select all the fragments that needs 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 initialise 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.checkout_graph and not R.navigation.checkout_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 is a bug.

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

Thanks, OP and @Archie for pointing me in the right direction.

hushed_voice
  • 1,836
  • 20
  • 41
1

Im just writing library to address this problem: scoped-vm, feel free to check it out and I will highly appreciate any feedback. Under the hood, it uses the approach @Archie mentioned - it maintains separate ViewModelStore per scope. But it goes one step further and clears ViewModelStore itself as soon as the last fragment that requested viewmodel from that scope destroys.

I should say that currently whole viewmodel management (and this lib particularly) is affected with a serious bug with the backstack, hopefully it will be fixed.

Summary:

  • If you care about ViewModel.onCleared() not being called, the best way (for now) is to clear it yourself. Because of that bug, you have no guaranty that viewmodel of a fragment will ever be cleared.
  • If you just worry about leaked ViewModel - do not worry, they will be garbage collected as any other non-referenced objects. Feel free to use my lib for fine-grained scoping, if it suits your needs.
dhabensky
  • 925
  • 6
  • 13
  • I've implemented subscriptions - each time fragment requests a viewModel subscription is created. Subscriptions are viewmodels themselves and kept in that fragment's ViewModelStore thus get cleared automatically. Subscription that extends ViewModel is the most beautiful and ugly part of a library at the same time! – dhabensky Apr 09 '19 at 09:23
  • Sounds interesting! Update me with it from time to time. I'd probably check it out one of these days. :) – Archie G. Quiñones Apr 09 '19 at 13:01
  • 1
    @ArchieG.Quiñones Just released fresh new version 0.4. Lifecycle-viewmodel bug seems to be fixed somewhere in the nearest future, cause it got P1 priority and there are [recent changes](https://android-review.googlesource.com/c/platform/frameworks/support/+/941796/) in the repository. Once it gets fixed I plan to go 1.0 – dhabensky Apr 13 '19 at 07:18
1

As it was pointed out it is not possible to clear an individual ViewModel of a ViewModelStore using the architecture components API. One possible solution to this issue is having a per-ViewModel stores that can be safely cleared when necessary:

class MainActivity : AppCompatActivity() {

val individualModelStores = HashMap<KClass<out ViewModel>, ViewModelStore>()

inline fun <reified VIEWMODEL : ViewModel> getSharedViewModel(): VIEWMODEL {
    val factory = object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            //Put your existing ViewModel instantiation code here,
            //e.g., dependency injection or a factory you're using
            //For the simplicity of example let's assume
            //that your ViewModel doesn't take any arguments
            return modelClass.newInstance()
        }
    }

    val viewModelStore = this@MainActivity.getIndividualViewModelStore<VIEWMODEL>()
    return ViewModelProvider(this.getIndividualViewModelStore<VIEWMODEL>(), factory).get(VIEWMODEL::class.java)
}

    val viewModelStore = this@MainActivity.getIndividualViewModelStore<VIEWMODEL>()
    return ViewModelProvider(this.getIndividualViewModelStore<VIEWMODEL>(), factory).get(VIEWMODEL::class.java)
}

inline fun <reified VIEWMODEL : ViewModel> getIndividualViewModelStore(): ViewModelStore {
    val viewModelKey = VIEWMODEL::class
    var viewModelStore = individualModelStores[viewModelKey]
    return if (viewModelStore != null) {
        viewModelStore
    } else {
        viewModelStore = ViewModelStore()
        individualModelStores[viewModelKey] = viewModelStore
        return viewModelStore
    }
}

inline fun <reified VIEWMODEL : ViewModel> clearIndividualViewModelStore() {
    val viewModelKey = VIEWMODEL::class
    individualModelStores[viewModelKey]?.clear()
    individualModelStores.remove(viewModelKey)
}

}

Use getSharedViewModel() to obtain an instance of ViewModel which is bound to the Activity's lifecycle:

val yourViewModel : YourViewModel = (requireActivity() as MainActivity).getSharedViewModel(/*There could be some arguments in case of a more complex ViewModelProvider.Factory implementation*/)

Later, when it's the time to dispose the shared ViewModel, use clearIndividualViewModelStore<>():

(requireActivity() as MainActivity).clearIndividualViewModelStore<YourViewModel>()

In some cases you would want to clear the ViewModel as soon as possible if it's not needed anymore (e.g., in case of it containing some sensitive user data like username or password). Here's a way of logging the state of individualModelStores upon every fragment switching to help you keep track of shared ViewModels:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    if (BuildConfig.DEBUG) {
        navController.addOnDestinationChangedListener { _, _, _ ->
            if (individualModelStores.isNotEmpty()) {
                val tag = this@MainActivity.javaClass.simpleName
                Log.w(
                        tag,
                        "Don't forget to clear the shared ViewModelStores if they are not needed anymore."
                )
                Log.w(
                        tag,
                        "Currently there are ${individualModelStores.keys.size} ViewModelStores bound to ${this@MainActivity.javaClass.simpleName}:"
                )
                for ((index, viewModelClass) in individualModelStores.keys.withIndex()) {
                    Log.w(
                            tag,
                            "${index + 1}) $viewModelClass\n"
                    )
                }
            }
        }
    }
}
Alex Kuzmin
  • 566
  • 4
  • 14
1

I found a simple and fairly elegant way to deal with this issue. The trick is to use a DummyViewModel and model key.

The code works because AndroidX checks the class type of the model on get(). If it doesn't match it creates a new ViewModel using the current ViewModelProvider.Factory.

public class MyActivity extends AppCompatActivity {
    private static final String KEY_MY_MODEL = "model";

    void clearMyViewModel() {
        new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).
            .get(KEY_MY_MODEL, DummyViewModel.class);
    }

    MyViewModel getMyViewModel() {
        return new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication()).
            .get(KEY_MY_MODEL, MyViewModel.class);
    }

    static class DummyViewModel extends ViewModel {
        //Intentionally blank
    }
}   
Dustin
  • 1,824
  • 1
  • 14
  • 11
1

In my case, most of the things I observe are related to the Views, so I don't need to clear it in case the View gets destroyed (but not the Fragment).

In the case I need things like a LiveData that takes me to another Fragment (or that does the thing only once), I create a "consuming observer".

It can be done by extending MutableLiveData<T>:

fun <T> MutableLiveData<T>.observeConsuming(viewLifecycleOwner: LifecycleOwner, function: (T) -> Unit) {
    observe(viewLifecycleOwner, Observer<T> {
        function(it ?: return@Observer)
        value = null
    })
}

and as soon as it's observed, it will clear from the LiveData.

Now you can call it like:

viewModel.navigation.observeConsuming(viewLifecycleOwner) { 
    startActivity(Intent(this, LoginActivity::class.java))
}
Rafael Ruiz Muñoz
  • 4,769
  • 6
  • 41
  • 79
0

As I know you can't remove ViewModel object manually by program, but you can clear data that stored in that,for this case you should call Oncleared() method manually for doing this:

  1. Override Oncleared() method in that class that is extended from ViewModel class
  2. In this method you can clean data by making null the field that you store data in it
  3. Call this method when you want clear data completely.
EstevaoLuis
  • 1,902
  • 7
  • 28
  • 35
Amir Hossein
  • 37
  • 1
  • 7
0

It seems like it has been already solved in the latest architecture components version.

ViewModelProvider has a following constructor:

    /**
 * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
 * {@code Factory} and retain them in a store of the given {@code ViewModelStoreOwner}.
 *
 * @param owner   a {@code ViewModelStoreOwner} whose {@link ViewModelStore} will be used to
 *                retain {@code ViewModels}
 * @param factory a {@code Factory} which will be used to instantiate
 *                new {@code ViewModels}
 */
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
    this(owner.getViewModelStore(), factory);
}

Which, in case of Fragment, would use scoped ViewModelStore.

androidx.fragment.app.Fragment#getViewModelStore

    /**
 * Returns the {@link ViewModelStore} associated with this Fragment
 * <p>
 * Overriding this method is no longer supported and this method will be made
 * <code>final</code> in a future version of Fragment.
 *
 * @return a {@code ViewModelStore}
 * @throws IllegalStateException if called before the Fragment is attached i.e., before
 * onAttach().
 */
@NonNull
@Override
public ViewModelStore getViewModelStore() {
    if (mFragmentManager == null) {
        throw new IllegalStateException("Can't access ViewModels from detached fragment");
    }
    return mFragmentManager.getViewModelStore(this);
}

androidx.fragment.app.FragmentManagerViewModel#getViewModelStore

    @NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
    ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
    if (viewModelStore == null) {
        viewModelStore = new ViewModelStore();
        mViewModelStores.put(f.mWho, viewModelStore);
    }
    return viewModelStore;
}
Oleksandr
  • 3,552
  • 8
  • 43
  • 78
-3

Typically you don't clear the ViewModel manually, because it is handled automatically. If you feel the need to clear your ViewModel manually, you're probably doing too much in that ViewModel...

There's nothing wrong with using multiple viewmodels. First one could be scoped to the Activity while another one could be scoped to the fragment.

Try to use the Activity scoped Viewmodel only for things that need to be shared. And put as many things as possible in the Fragment Scoped Viewmodel. The Fragment scoped viewmodel will be cleared when the fragment is destroyed. Reducing the overall memory footprint.

Entreco
  • 11,914
  • 8
  • 70
  • 88
  • Agreed, this is better than sharing all information. However in a single-activity app it means there is still potentially a lot of ViewModel memory retained through the whole life of the app. I'm looking to optimise that and free it up if possible. – Richard Le Mesurier Dec 06 '18 at 14:24
  • 2
    "doing too much in that ViewModel" - I disagree since in the single activity framework that ViewModel will live forever. In theory any amount of memory that cannot ever be freed, even though not in use, is not optimal. While "in the real world" we can get away with leaking a few bytes/kilobytes I don't think that should be best practice. – Richard Le Mesurier Dec 06 '18 at 14:48
  • I disagree. Even in the single-activity app, you should NOT manually clear your ViewModel. You should clear `fields` that are no longer needed -> true. But never call `clear()` on the ViewModel itself. If you need to do that, you don't need a ViewModel – Entreco Jan 10 '19 at 14:50
  • 2
    Always good to have a disagreement of opinions. But I still feel it is less than optimal to have lots of shared, empty and unused `ViewModel` instances floating around the store. In general I think a lot of the new Jetpack stuff still has some very rough edges and I'm hoping for major enhancements in the near future. – Richard Le Mesurier Jan 11 '19 at 11:11
  • Now that i think about it, ViewModels are just "Retained Fragments" renamed to "ViewModel" (thats an over simplefication but you get what i mean) so just as you may call, fragment.remove to remove a retained fragment, the same should be accepted by clearing a viewmodel. So essentially, "Retained Fragnent.remove()" is just "viewmodelstore.clear()". Does anyone think so too? – Archie G. Quiñones Feb 23 '19 at 04:53