手动清除 Android 视图模型?

编辑: 这个问题现在有点过时了,因为 Google 已经给了我们将 ViewModel范围扩展到导航图的能力。更好的方法(而不是试图清除活动范围的模型)是为适当数量的屏幕创建特定的导航图,以及这些屏幕的范围。


引用 android.arch.lifecycle.ViewModel类。

ViewModel的作用域是它所涉及的 UI 组件的生命周期,因此在基于 Fragment的应用程序中,这将是片段生命周期。这是件好事。


在某些情况下,人们希望在多个片段之间共享一个 ViewModel实例。具体来说,我感兴趣的情况下,许多屏幕与相同的底层数据相关

(当多个相关的片段显示在同一个屏幕上,而不是 这可以通过使用单个主机片段来解决,如下面的答案所示上时,医生建议采用类似的方法。)

官方 ViewModel 文档讨论了这一点:

视图模型还可以用作不同 活动的片段。每个片段都可以获得 ViewModel 通过他们的活动使用相同的键。这允许通信 在碎片之间以解耦的方式,使他们永远不需要 直接和另一个碎片对话。

换句话说,为了在代表不同屏幕的片段之间共享信息,ViewModel应该作用于 Activity生命周期(根据 Android 文档,这也可以用于其他共享实例)。


现在,在新的 Jetpack 导航模式中,建议使用“一个活动/多个片段”体系结构。这意味着活动在应用程序被使用的整个时间都存在。

也就是说,任何作用域为 Activity生命周期的共享 ViewModel实例将永远不会被清除——内存仍在持续使用。

为了保存内存,并且在任何时候都尽可能少地使用需要的内存,如果能够在不再需要时清除共享的 ViewModel实例就好了。


如何手动清除 ViewModel从它的 ViewModelStore或持有人片段?

58458 次浏览

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!

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.

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.

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.

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.

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"
)
}
}
}
}
}

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
}
}

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

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.

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))
}

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.

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;
}