手动清除 Android ViewModel?

Manually clearing an Android ViewModel?

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


参照android.arch.lifecycle.ViewModel class.

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


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

(文档建议在同一屏幕上显示多个相关片段但 时使用类似的方法。)

这在 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.

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


现在在新的 Jetpack Navigation 模式中,推荐使用 "One Activity / Many Fragments" 架构。这意味着 activity 在应用程序被使用的整个过程中都存在。

即任何范围为 Activity 生命周期的共享 ViewModel 实例将永远不会被清除 - 内存一直在使用。

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


如何从 ViewModelStore 或 holder 片段中手动清除 ViewModel

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

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

希望这对您有所帮助!

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

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

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

如果您查看代码 here,您会发现,您可以从 ViewModelStoreOwnerFragmentFragmentActivity 中获取 ViewModelStore例如实现,那个接口。

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

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

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

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

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

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

请就此方法提供一些反馈。任何东西都会受到赞赏。

更新:

Navigation Component v2.1.0-alpha02 以来,ViewModels 现在可以限定为流。这样做的缺点是您必须对您的项目实施 Navigation Component,而且您无法对 ViewModel 的范围进行精细控制。但这似乎是更好的事情。

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

我应该说目前整个视图模型管理(尤其是这个库)受到 serious bug 与后台堆栈的影响,希望它会被修复。

总结:

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

正如所指出的那样,使用架构组件 API 无法清除 ViewModelStore 的单个 ViewModel。此问题的一种可能解决方案是拥有一个 per-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"
                    )
                }
            }
        }
    }
}

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

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

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

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

这将在不合并 Navigation Component 库的情况下解决此问题。这也是简单的一行代码。它将通过 Activity

清除 Fragments 之间共享的那些 ViewModels

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

  1. 重写从 ViewModel class
  2. 扩展而来的 class 中的 Oncleared() 方法
  3. 在此方法中,您可以通过将存储数据的字段设为 null 来清理数据
  4. 当你想彻底清除数据时调用此方法。

在我的例子中,我观察到的大多数事情都与 View 相关,所以我不需要清除它以防 View 被破坏(而不是 Fragment).

如果我需要 LiveData 之类的东西将我带到另一个 Fragment(或者只做一次),我会创建一个 "consuming observer".

可以通过扩展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))
}

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

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

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

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

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

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