编辑:这个问题有点过时了,因为谷歌已经让我们能够将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
如果你检查这里的代码,你会发现你可以从ViewModelStoreOwner
和Fragment
中获得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
}
}
在我的情况下,我观察到的大多数东西都与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))
}
正如我所知,你不能通过程序手动删除ViewModel对象,但你可以清除存储在其中的数据,在这种情况下,你应该手动调用onCleared()
方法这样做:
- 覆盖该类中从
ViewModel
类扩展而来的onCleared()
方法 - 在这种方法中,您可以通过将存储数据的字段设为null来清除数据
- 当您想要完全清除数据时,请调用此方法
通常不手动清除ViewModel,因为它是自动处理的。如果您觉得需要手动清除ViewModel,那么您可能在该ViewModel中做了太多。。。
使用多个视图模型没有错。第一个可以确定活动的范围,而另一个可以确定片段的范围。
尝试仅将活动范围的视图模型用于需要共享的内容。并在片段范围视图模型中放入尽可能多的内容。当片段被销毁时,片段范围的视图模型将被清除。减少总体内存占用。