手动清除 Android ViewModel?

105

编辑:现在谷歌已经为我们提供了将ViewModel作用于导航图的能力,因此这个问题有点过时了。更好的方法(而不是尝试清除活动范围的模型)是为正确数量的屏幕创建特定的导航图,并将其作用于这些导航图。


关于android.arch.lifecycle.ViewModel类。

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


在某些情况下,我们希望在多个Fragment之间共享一个ViewModel实例。特别是当许多屏幕相关联的底层数据相同时。这在官方的ViewModel文档中有所讨论:

ViewModel还可以用作Activity中不同Fragment之间的通信层。每个Fragment都可以通过其Activity使用相同的键获取ViewModel。这样可以以解耦合的方式在Fragment之间进行通信,从而无需直接与另一个Fragment进行对话。

换句话说,为了在表示不同屏幕的Fragment之间共享信息,ViewModel应该被范围限定到Activity的生命周期内(并且根据Android文档,在其他共享实例中也可以使用)。此外,文档建议类似的方法可在同一屏幕上显示多个相关Fragment时使用,但这可以通过以下答案中的单个主机Fragment来解决
现在,在新的Jetpack Navigation模式中,建议使用"一个Activity/多个Fragment"架构。这意味着Activity会在应用程序使用的整个时间内一直存在。
例如,任何作用域为Activity生命周期的共享ViewModel实例将永远不会被清除,内存始终处于占用状态。为了保留内存并在任何时候尽可能少地使用它,最好能够在不再需要时清除共享的ViewModel实例。
如何手动从其ViewModelStore或持有片段中清除ViewModel?

相关:Android JetPack的共享ViewModel生命周期 - Richard Le Mesurier
1
嘿!创建自己的保留片段并将您的视图模型限定在该保留片段中如何?现在,您可以完全控制视图模型的生命周期。您只需要让活动根据需要添加或删除片段,并通过活动将保留片段和其他片段连接在一起即可。虽然听起来像是编写一些样板代码,但我想知道您的想法。 - Archie G. Quiñones
我不确定是否可以使用getTargetFragment()来限定范围: ViewModelProvider(requireNotNull(targetFragment)).get(MyViewModel::class.java) - user4003256
是的,有一种方法可以这样做,我在这里进行了解释。 - Mostafa Arian Nejad
1
对于那些尝试实现更新解决方案的人,请前往此处:https://medium.com/androiddevelopers/viewmodels-with-saved-state-jetpack-navigation-data-binding-and-coroutines-df476b78144e - hushed_voice
13个回答

44

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

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

通过这一行简单的代码,即可解决该问题,无需使用Navigation Component库。该代码可以清除在Activity中通过共享ViewModelsFragments之间传递的内容。


请注意,它正在清除所有视图模型数据存储,而不仅仅是一个片段视图模型。 - Moustafa EL-Saghier
3
@MoustafaEL-Saghier - 是的,这是真的。它将清除在Activity之间共享的所有ViewModels - Sakiboy

31
如果您检查代码此处,您会发现可以从ViewModelStoreOwnerFragmentFragmentActivity中获取ViewModelStore,例如FragmentActivity实现了该接口。

因此,您可以调用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。


非常好,我正朝着这个方向寻找答案,但错过了显而易见的部分,就像你所说的“FragmentActivity...实现了该接口[ViewModelStoreOwner]”。 - Richard Le Mesurier
1
好的,我们可以手动清除ViewModel,但这是个好主意吗?如果我通过这种方法清除视图模型,有什么需要注意或确保我正确执行的事情吗? - Archie G. Quiñones
我还注意到您无法仅清除特定的ViewModel,这应该是情况。如果您调用viewmodelstoreowner.clear(),则将清除所有已存储的ViewModel。 - Archie G. Quiñones
1
需要注意的是,如果您正在使用新的SavedStateViewModelFactory来创建特定的视图模型,则需要调用savedStateRegistry.unregisterSavedStateProvider(key) - 其中的key应该是在调用ViewModelProvider(~).get(key, class)时使用的那个。否则,如果您尝试在将来获取(即创建)视图模型,则会收到“IllegalArgumentException:已注册具有给定键的SavedStateProvider”的错误提示。 - hmac

18

正如OP和Archie所说,Google已经赋予我们将ViewModel作用域限定在导航图中的能力。如果您已经使用了导航组件,则我将在此处添加如何执行此操作。

您可以选择需要在导航图中分组的所有片段,然后右键单击->移动到嵌套图形->新图形

现在,这将把选定的片段移动到主导航图中的嵌套图中,就像这样:

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

现在,在初始化 viewmodel 时,请在片段内执行此操作

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

如果您需要传递ViewModel工厂(可能是为了注入ViewModel),可以按照以下方式进行:

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指引我正确的方向。


2
是的,我只是想强调“id”部分。 - hushed_voice
好东西。我不想跳进来自己改动它,以防那是有意的。 - Richard Le Mesurier
你似乎无法传递参数。子图包含片段的操作,但它没有正确生成方向以获取参数。 - Brill Pappin

10

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

正如@Nagy Robi所述,您可以通过调用viewModelStore.clear()来清除ViewModel。但问题在于,它将清除此ViewModelStore中的所有视图模型。换句话说,您无法控制要清除哪个ViewModel

但是根据@mikehc在这里的说法,我们实际上可以创建自己的ViewModelStore。这将使我们对ViewModel存在的范围具有粒度控制。

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

请就此方法提供一些反馈。任何意见都将不胜感激。

更新:

Navigation Component v2.1.0-alpha02以来,ViewModel现在可以针对流进行范围限定。但其缺点是您必须将Navigation Component实现到您的项目中,并且对于您的ViewModel的范围,您没有粒度控制。但这似乎是一件更好的事情。


1
是的,你说得对,Archie G。我认为一般来说我们不应该手动清除虚拟机,而将其限定于导航图提供了一种非常好的、清晰的处理ViewModel作用域的方式。 - Róbert Nagy
对于试图实现更新解决方案的人,请前往此处:https://medium.com/androiddevelopers/viewmodels-with-saved-state-jetpack-navigation-data-binding-and-coroutines-df476b78144e - hushed_voice

9

如果您不想将ViewModel限定于Activity的生命周期,则可以将其限定于父片段的生命周期。因此,如果您想要在屏幕上与多个片段共享ViewModel的实例,则可以布置这些片段,使它们都共享一个公共父片段。这样当您实例化ViewModel时,只需执行以下操作:

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

希望这能有所帮助!

1
你所写的是正确的,但这是针对我想要将其限定在“Activity”生命周期范围内的情况,特别是在多个可能不同时显示的片段之间共享它。这是我提到的另一种情况的好回答,我认为我必须更新我的问题以删除那种情况(因为它会造成混淆-对此表示歉意)。 - Richard Le Mesurier

2

最新的架构组件版本似乎已经解决了这个问题。

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

1
没错,这样viewModel就可以与Fragment绑定,而不是Activity。 - Vadim Kotov

2
我找到了一个简单而优雅的方法来处理这个问题。诀窍是使用DummyViewModel和model key。
代码之所以有效,是因为AndroidX在获取时检查模型的类类型。如果不匹配,则使用当前的ViewModelProvider.Factory创建一个新的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
    }
}   

最快的解决方案也是最干净的。另一个解决方案可能是使用navgraph scoped ViewModels,但在生产应用中整合起来太过繁琐。 - undefined

1
我只是在编写一个库来解决这个问题:scoped-vm,欢迎查看并非常感谢任何反馈。 在底层,它使用了@Archie提到的方法——它为每个作用域维护单独的ViewModelStore。但它更进一步,当从该范围请求viewmodel的最后一个片段销毁时,它会清除ViewModelStore本身。 我应该说,目前整个viewmodel管理(特别是这个库)都受到了严重的错误的影响,希望能够修复。 总结:
  • 如果你在意 ViewModel.onCleared() 方法没有被调用,目前最好的方法是自己清除。由于该bug存在,你无法保证fragment的viewmodel会被清除。
  • 如果你只担心泄露的ViewModel,不用担心,它们将像其他非引用对象一样被垃圾回收。如果需要,可以随时使用我的库进行细粒度作用域控制。

我已经实现了订阅功能 - 每次片段请求viewModel时都会创建一个订阅。订阅本身是viewmodel,并保存在该片段的ViewModelStore中,因此会自动清除。 继承ViewModel的订阅是库中最美丽和丑陋的部分! - dhabensky
听起来很有趣!时不时地更新我。我可能会在这些日子里之一去看看。 :) - Archie G. Quiñones
1
@ArchieG.Quiñones 刚刚发布了全新的 0.4 版本。由于 Lifecycle-viewmodel bug 已经被赋予 P1 优先级,并且代码库中有最近的更改,因此该问题很快就会得到解决。一旦它被修复,我计划将其升级至 1.0 版本。 - dhabensky

1
正如指出的那样,使用架构组件API无法清除单个ViewModel的ViewModelStore。解决此问题的一种可能方案是拥有每个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()获取一个与Activity生命周期绑定的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状态以帮助您跟踪共享ViewModel的方法:

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

1
在我的情况下,我观察到的大部分事物都与View有关,因此如果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))
}

SDK里面没有内置的解决方案吗? - IgorGanapolsky
我认为 ViewModel 不应该像那样使用。它更多地用于保存数据,即使视图被销毁(但不是 Fragment),因此您可以恢复所有信息。 - Rafael Ruiz Muñoz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接