手动清除Android ViewModel



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


参考android.arch.lifecycle.ViewModel类。

ViewModel的作用域是与其相关的UI组件的生命周期,因此在基于Fragment的应用程序中,这将是片段生命周期。这是一件好事。


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

(当多个相关片段显示在同一屏幕上时,文档建议使用类似的方法,但根据下面的答案,可以通过使用单个主机片段来解决。)

这在官方ViewModel文档中进行了讨论:

ViewModels还可以用作不同活动的片段。每个片段都可以获取ViewModel通过他们的"活动"使用相同的键。这允许通信以解耦的方式在碎片之间,这样它们就不需要直接与另一个片段对话。

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


现在在新的Jetpack导航模式中,建议使用"一个活动/多个片段"架构。这意味着该活动在应用程序使用的整个时间内都有效。

即,范围为Activity生命周期的任何共享ViewModel实例将永远不会被清除——内存仍在持续使用中。

为了保留内存并在任何时间点尽可能少地使用,在不再需要时能够清除共享的ViewModel实例将是一件好事。


如何手动从ViewModelStore或持有者片段中清除ViewModel

无需使用Navigation Component库的快速解决方案:

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

这将在不合并Navigation Component库的情况下解决这个问题。这也是一行简单的代码。它将通过Activity清除Fragments之间共享的ViewModels

如果你检查这里的代码,你会发现你可以从ViewModelStoreOwnerFragment中获得ViewModelStore,例如FragmentActivity实现了该接口。

Soo从那里你可以打电话给viewModelStore.clear(),正如文件所说:

/**
*  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();
}

注意:这将清除特定LifeCycleOwner的所有可用ViewModel,不允许您清除一个特定ViewModel。

正如OP和Archie所说,谷歌为我们提供了将ViewModel范围扩展到导航图的能力。如果您已经在使用导航组件,我将在这里添加如何做到这一点。

您可以在导航图和right-click->move to nested graph->new graph中选择需要分组在一起的所有片段

现在,这将把选定的片段移动到主导航图中的嵌套图中,如下所示:

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

现在,当你初始化视图模型时,在片段内部做这个

val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)

如果你需要通过视图模型工厂(可能是为了注入视图模型),你可以这样做:

val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph) { viewModelFactory }

确保其R.id.checkout_graph而非R.navigation.checkout_graph

出于某种原因,创建导航图并使用include将其嵌套在主导航图中对我来说不起作用。这可能是一个错误。

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

感谢OP和@Archie为我指明了正确的方向。

我认为我有一个更好的解决方案。

正如@Nagy Robi所说,您可以通过调用viewModelStore.clear()来清除ViewModel。这样做的问题是,它将清除此ViewModelStore范围内的所有视图模型。换句话说,您将无法控制要清除哪个ViewModel

但根据@mikehc的说法。实际上,我们可以创建自己的ViewModelStore。这将允许我们对ViewModel必须存在的范围进行细粒度控制。

注意:我没有看到任何人这样做,但我希望这是一个有效的方法。这将是控制单个活动应用程序中作用域的一种非常好的方法。

请对这种方法给予一些反馈。任何事情都将不胜感激。

更新:

自从Navigation Component v2.1.0-alpha02以来,ViewModel的作用域现在可以定为流。这样做的缺点是,您必须将Navigation Component实现到您的项目中,而且您无法对ViewModel的范围进行全局控制。但这似乎是一件更好的事情。

如果不希望ViewModel的作用域为Activity生命周期,可以将其作用域为父片段的生命周期。所以,如果您想在一个屏幕中与多个片段共享ViewModel的一个实例,您可以对这些片段进行布局,使它们共享一个共同的父片段。这样,当你实例化ViewModel时,你就可以这样做:

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

希望这能有所帮助!

在最新的体系结构组件版本中似乎已经解决了这个问题。

ViewModelProvider具有以下构造函数:

/**
* 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);
}

在Fragment的情况下,它将使用作用域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;
}

我只是在编写库来解决这个问题:scoped vm,请随意查看,我将非常感谢任何反馈。在引擎盖下,它使用了@Archie提到的方法——它为每个作用域维护单独的ViewModelStore。但它更进一步,在从该范围请求视图模型的最后一个片段销毁后,它会立即清除ViewModelStore本身。

我应该说,目前整个视图模型管理(尤其是这个库)都受到了后台严重错误的影响,希望它能得到修复。

摘要:

  • 如果你关心ViewModel.onCleared()没有被调用,(目前)最好的方法是自己清除。由于这个错误,您不能保证fragment的视图模型会被清除
  • 如果您只是担心泄露的ViewModel——不要担心,它们将作为任何其他未引用的对象被垃圾收集。如果适合您的需要,可以随意使用我的lib进行细粒度范围界定

正如所指出的,使用体系结构组件API无法清除ViewModelStore的单个ViewModel。这个问题的一个可能的解决方案是有一个按ViewModel存储,必要时可以安全清除:

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

}

使用getSharedViewModel()获取绑定到活动生命周期的ViewModel实例:

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

稍后,当需要处理共享ViewModel时,请使用clearIndividualViewModelStore<>():

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

在某些情况下,如果不再需要ViewModel,您会希望尽快清除它(例如,如果它包含一些敏感的用户数据,如用户名或密码)。以下是一种在每次片段切换时记录individualModelStores状态的方法,可以帮助您跟踪共享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}) $viewModelClassn"
)
}
}
}
}
}

我找到了一种简单而优雅的方法来处理这个问题。诀窍是使用DummyViewModel和模型密钥。

代码之所以有效,是因为AndroidX在get()上检查模型的类类型。如果不匹配,则使用当前ViewModelProvider创建一个新的ViewModel。工厂

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

在我的情况下,我观察到的大多数东西都与Views有关,所以我不需要清除它,以防View(但不是Fragment)被破坏。

如果我需要像LiveData这样的东西,它会把我带到另一个Fragment(或者只做一次),我会创建一个"消费观察者"。

可以通过扩展MutableLiveData<T>:来实现

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

并且一旦观察到它就会从LiveData中清除。

现在你可以这样称呼它:

viewModel.navigation.observeConsuming(viewLifecycleOwner) { 
startActivity(Intent(this, LoginActivity::class.java))
}

正如我所知,你不能通过程序手动删除ViewModel对象,但你可以清除存储在其中的数据,在这种情况下,你应该手动调用onCleared()方法这样做:

  1. 覆盖该类中从ViewModel类扩展而来的onCleared()方法
  2. 在这种方法中,您可以通过将存储数据的字段设为null来清除数据
  3. 当您想要完全清除数据时,请调用此方法

通常不手动清除ViewModel,因为它是自动处理的。如果您觉得需要手动清除ViewModel,那么您可能在该ViewModel中做了太多。。。

使用多个视图模型没有错。第一个可以确定活动的范围,而另一个可以确定片段的范围。

尝试仅将活动范围的视图模型用于需要共享的内容。并在片段范围视图模型中放入尽可能多的内容。当片段被销毁时,片段范围的视图模型将被清除。减少总体内存占用。

最新更新