手动清除 Android ViewModel?

Posted

技术标签:

【中文标题】手动清除 Android ViewModel?【英文标题】:Manually clearing an Android ViewModel? 【发布时间】:2019-05-08 06:05:09 【问题描述】:

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


参考android.arch.lifecycle.ViewModel 类。

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


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

(当多个相关片段显示在同一屏幕上但this can be worked around by using a single host fragment as per answer below时,文档建议类似的方法。)

这在official ViewModel documentation中讨论:

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

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


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

即范围为Activity 生命周期的任何共享ViewModel 实例将永远不会被清除 - 内存保持持续使用状态。

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


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

【问题讨论】:

相关:Shared ViewModel lifecycle for Android JetPack 嘿!如何创建您自己的保留片段并将您的视图模型限定为该保留片段?现在,您可以完全控制视图模型的生命周期。如果需要,您只需要使活动添加或删除片段,并通过活动将保留的片段和其他片段连接在一起。听起来确实像是在写一些样板代码,但我想知道你的想法。 我不知道是否可以使用 getTargetFragment() 作为范围:ViewModelProvider(requireNotNull(targetFragment)).get(MyViewModel::class.java) 是的,有办法,我已经解释过了here 对于尝试实施更新解决方案的人,请转到此处medium.com/androiddevelopers/… 【参考方案1】:

如果您不希望将ViewModel 限定为Activity 生命周期,您可以将其限定为父片段的生命周期。因此,如果您想在一个屏幕中与多个片段共享ViewModel 的实例,您可以对片段进行布局,使它们都共享一个公共父片段。这样,当您实例化 ViewModel 时,您可以这样做:

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

希望这会有所帮助!

【讨论】:

您写的是真的,但这是针对我确实想将其范围限定为 Activity 生命周期的情况,特别是在可能不会同时显示的多个片段之间共享它.在我提到的另一种情况下,这是一个很好的回应,我认为我必须更新我的问题以删除该情况(因为它会造成混乱 - 对此表示歉意)【参考方案2】:

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

使用多个视图模型并没有错。第一个可以限定为 Activity,而另一个可以限定为片段。

尝试仅将 Activity 范围的 Viewmodel 用于需要共享的内容。并在 Fragment Scoped Viewmodel 中放置尽可能多的东西。当片段被销毁时,片段范围的视图模型将被清除。减少整体内存占用。

【讨论】:

同意,这比共享所有信息要好。然而,在单一活动应用程序中,这意味着在应用程序的整个生命周期中仍可能保留大量 ViewModel 内存。如果可能的话,我希望优化它并释放它。 “在那个 ViewModel 中做得太多” - 我不同意,因为在单一活动框架中 ViewModel 将永远存在。从理论上讲,任何无法释放的内存量,即使没有被使用,也不是最优的。虽然“在现实世界中”我们可以避免泄漏几个字节/千字节,但我认为这不应该是最佳实践。 我不同意。即使在单活动应用程序中,也不应手动清除 ViewModel。您应该清除不再需要的fields -> true。但永远不要在 ViewModel 本身上调用clear()。如果您需要这样做,则不需要 ViewModel 有不同意见总是好的。但我仍然觉得让大量共享的、空的和未使用的ViewModel 实例漂浮在商店周围并不是最佳选择。总的来说,我认为很多新的 Jetpack 东西仍然有一些非常粗糙的边缘,我希望在不久的将来会有重大改进。 现在我想起来了,ViewModel 只是重命名为“ViewModel”的“保留片段”(这是一个过于简单化,但你明白我的意思)所以正如你所说的,fragment.remove 到删除保留的片段,同样应该通过清除视图模型来接受。所以本质上,“Retained Fragnent.remove()”就是“viewmodelstore.clear()”。有人也这么认为吗?【参考方案3】:

如果您检查代码here,您会发现,您可以从ViewModelStoreOwnerFragment 获得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]”。 好的,我们可以手动清除 ViewModel,但这是个好主意吗?如果我通过这种方法清除视图模型,我应该注意什么或确保我做得正确吗? 我还注意到您不能只清除应该是这种情况的特定视图模型。如果您调用 viewmodelstoreowner.clear() 所有存储的视图模型将被清除。 一个警告,如果你使用新的SavedStateViewModelFactory来创建一个特定的视图模型,你需要调用savedStateRegistry.unregisterSavedStateProvider(key)——关键是你应该使用的那个致电ViewModelProvider(~).get(key, class)。否则,如果您将来尝试获取(即创建)视图模型,您将获得 IllegalArgumentException: SavedStateProvider with the given key is already registered【参考方案4】:

我想我有更好的解决方案。

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

但根据@mikehc here。我们实际上可以创建我们自己的ViewModelStore。这将使我们能够精细控制 ViewModel 必须存在的范围。

注意:我没有看到有人这样做,但我希望这是一个有效的方法。这将是在单 Activity 应用程序中控制范围的好方法。

请对此方法提供一些反馈。任何事情都将不胜感激。

更新:

由于Navigation Component v2.1.0-alpha02,ViewModels 现在可以限定为流。这样做的缺点是您必须在您的项目中实施Navigation Component,而且您对ViewModel 的范围没有精确的控制。但这似乎是一件更好的事情。

【讨论】:

是的,你是对的 Archie G。我认为一般来说我们不应该手动清除虚拟机,导航图的范围提供了一种非常好的和干净的方式来处理 ViewModel 的范围 对于尝试实施更新解决方案的人,请访问此处:medium.com/androiddevelopers/…【参考方案5】:

我正在编写库来解决这个问题:scoped-vm,请随时查看,我将非常感谢任何反馈。 在后台,它使用提到的方法@Archie - 它为每个范围维护单独的 ViewModelStore。但它更进一步,一旦从该范围请求视图模型的最后一个片段销毁,就会清除 ViewModelStore 本身。

我应该说,目前整个视图模型管理(尤其是这个库)受到带有 backstack 的 serious bug 的影响,希望它会得到修复。

总结:

如果您关心ViewModel.onCleared() 未被调用,最好的方法(目前)是自己清除它。由于该错误,您无法保证 fragment 的视图模型将被清除。 如果您只是担心泄露ViewModel - 不用担心,它们将像任何其他非引用对象一样被垃圾回收。如果适合您的需要,请随意使用我的 lib 进行细粒度范围界定。

【讨论】:

我已经实现了订阅 - 每次片段请求时都会创建一个 viewModel 订阅。订阅本身就是视图模型,并保存在该片段的 ViewModelStore 中,因此会自动清除。扩展 ViewModel 的订阅同时是库中最漂亮和最丑陋的部分! 听起来很有趣!不时更新我。我可能会在这些日子里检查一下。 :) @ArchieG.Quiñones 刚刚发布了全新的 0.4 版。 Lifecycle-viewmodel 错误似乎在最近的某个地方被修复,因为它获得了 P1 优先级并且存储库中有recent changes。一旦它得到修复,我计划去 1.0【参考方案6】:

正如所指出的,无法使用架构组件 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()获取绑定到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&lt;&gt;()

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

【讨论】:

【参考方案7】:

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

该代码有效,因为 AndroidX 在 get() 上检查模型的类类型。如果不匹配,它会使用当前的 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
    
   

【讨论】:

【参考方案8】:

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

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

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

【讨论】:

【参考方案9】:

据我所知,您无法通过程序手动删除 ViewModel 对象,但您可以清除其中存储的数据,对于这种情况,您应该手动调用 Oncleared() 方法 为此:

    覆盖从 ViewModel 类扩展的类中的 Oncleared() 方法 在此方法中,您可以通过将存储数据的字段设为 null 来清理数据 当你想彻底清除数据时调用这个方法。

【讨论】:

【参考方案10】:

就我而言,我观察到的大部分内容都与Views 有关,因此我不需要清除它以防View 被破坏(但不是Fragment)。

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

可以通过扩展MutableLiveData&lt;T&gt;来实现:

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中没有内置解决方案吗? 我认为ViewModel 不适合那样使用。即使视图被破坏(但不是 Fragment),也需要保存数据,因此您可以恢复所有信息【参考方案11】:

正如 OP 和 Archie 所说,Google 让我们能够将 ViewModel 限定为导航图。如果您已经在使用导航组件,我将在此处添加操作方法。

您可以在导航图和right-click-&gt;move to nested graph-&gt;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 为我指明了正确的方向。

【讨论】:

是的..我只是想强调“id”部分 好东西。不想跳进去自己改变它,以防万一。 你似乎不能用这个传递参数。子图包含片段的操作,但它没有正确生成方向以获取参数。【参考方案12】:

好像已经在最新的架构组件版本中解决了。

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;

【讨论】:

没错,这样viewModel可以绑定到Fragment,而不是Activity

以上是关于手动清除 Android ViewModel?的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法清除已经从Android设备手动捕获的位置详细信息?

Viewmode原理

使用 RelayCommand 从 ViewModel 中清除条目文本

Android---Jetpack之LiveData

ViewModel 在导航导航中没有被清除,并且 viewmodel 中的实时数据保持活动状态

如何从 ViewModel 一次清除 WPF 中的所有控件?