手动清除 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,您会发现,您可以从ViewModelStoreOwner
和Fragment
获得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,ViewModel
s 现在可以限定为流。这样做的缺点是您必须在您的项目中实施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<>()
:
(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】:就我而言,我观察到的大部分内容都与View
s 有关,因此我不需要清除它以防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中没有内置解决方案吗? 我认为ViewModel
不适合那样使用。即使视图被破坏(但不是 Fragment),也需要保存数据,因此您可以恢复所有信息【参考方案11】:
正如 OP 和 Archie 所说,Google 让我们能够将 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 为我指明了正确的方向。
【讨论】:
是的..我只是想强调“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设备手动捕获的位置详细信息?
使用 RelayCommand 从 ViewModel 中清除条目文本