问题
With reference to the android.arch.lifecycle.ViewModel
class.
ViewModel
is scoped to the lifecycle of the UI component it relates to, so in a Fragment
-based app, that will be the fragment lifecycle. This is a good thing.
In some cases one wants to share a ViewModel
instance between multiple fragments. Specifically I am interested in the case where many screens relate to the same underlying data.
(The docs suggest similar approach when multiple related fragments are displayed on the same screen but this can be worked around by using a single host fragment as per answer below.)
This is discussed in the 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.
In other words, to share information between fragments that represent different screens, the ViewModel
should be scoped to the Activity
lifecycle (and according to Android docs this can also be used in other shared instances).
Now in the new Jetpack Navigation pattern, it is recommended to use a "One Activity / Many Fragments" architecture. This means that the activity lives for the whole time the app is being used.
i.e. any shared ViewModel
instances that are scoped to Activity
lifecycle will never be cleared - the memory remains in constant use.
With a view to preserving memory and using as little as required at any point in time, it would be nice to be able to clear shared ViewModel
instances when no longer required.
How can one manually clear a ViewModel
from it's ViewModelStore
or holder fragment?
回答1:
If you check the code here you'll find out, that you can get the ViewModelStore
from a ViewModelStoreOwner
and Fragment
, FragmentActivity
for example implements, that interface.
Soo from there you could just call viewModelStore.clear()
, which as the documentation says:
/**
* 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.: This will clear all the available ViewModels for the specific LifeCycleOwner, this does not allow you to clear one specific ViewModel.
回答2:
If you don't want the ViewModel
to be scoped to the Activity
lifecycle, you can scope it to the parent fragment's lifecycle. So if you want to share an instance of the ViewModel
with multiple fragments in a screen, you can layout the fragments such that they all share a common parent fragment. That way when you instantiate the ViewModel
you can just do this:
CommonViewModel viewModel = ViewModelProviders.of(getParentFragment()).class(CommonViewModel.class);
Hopefully this helps!
回答3:
I think I have a better solution.
As stated by @Nagy Robi, you could clear the ViewModel
by call viewModelStore.clear()
. The problem with this is that it will clear ALL the view model scoped within this ViewModelStore
. In other words, you won't have control of which ViewModel
to clear.
But according to @mikehc here. We could actually create our very own ViewModelStore
instead. This will allow us granular control to what scope the ViewModel have to exist.
Note: I have not seen anyone do this approach but I hope this is a valid one. This will be a really good way to control scopes in a Single Activity Application.
Please give some feedbacks on this approach. Anything will be appreciated.
Update:
Since Navigation Component v2.1.0-alpha02, ViewModel
s could now be scoped to a flow. The downside to this is that you have to implement Navigation Component
to your project and also you have no granualar control to the scope of your ViewModel
. But this seems to be a better thing.
回答4:
Im just writing library to address this problem: scoped-vm, feel free to check it out and I will highly appreciate any feedback. Under the hood, it uses the approach @Archie mentioned - it maintains separate ViewModelStore per scope. But it goes one step further and clears ViewModelStore itself as soon as the last fragment that requested viewmodel from that scope destroys.
I should say that currently whole viewmodel management (and this lib particularly) is affected with a serious bug with the backstack, hopefully it will be fixed.
Summary:
- If you care about
ViewModel.onCleared()
not being called, the best way (for now) is to clear it yourself. Because of that bug, you have no guaranty that viewmodel of afragment
will ever be cleared. - If you just worry about leaked
ViewModel
- do not worry, they will be garbage collected as any other non-referenced objects. Feel free to use my lib for fine-grained scoping, if it suits your needs.
回答5:
As it was pointed out it is not possible to clear an individual ViewModel of a ViewModelStore using the architecture components API. One possible solution to this issue is having a per-ViewModel stores that can be safely cleared when necessary:
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)
}
}
Use getSharedViewModel()
to obtain an instance of ViewModel which is bound to the Activity's lifecycle:
val yourViewModel : YourViewModel = (requireActivity() as MainActivity).getSharedViewModel(/*There could be some arguments in case of a more complex ViewModelProvider.Factory implementation*/)
Later, when it's the time to dispose the shared ViewModel, use clearIndividualViewModelStore<>()
:
(requireActivity() as MainActivity).clearIndividualViewModelStore<YourViewModel>()
In some cases you would want to clear the ViewModel as soon as possible if it's not needed anymore (e.g., in case of it containing some sensitive user data like username or password). Here's a way of logging the state of individualModelStores
upon every fragment switching to help you keep track of shared 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}) $viewModelClass\n"
)
}
}
}
}
}
回答6:
Typically you don't clear the ViewModel manually, because it is handled automatically. If you feel the need to clear your ViewModel manually, you're probably doing too much in that ViewModel...
There's nothing wrong with using multiple viewmodels. First one could be scoped to the Activity while another one could be scoped to the fragment.
Try to use the Activity scoped Viewmodel only for things that need to be shared. And put as many things as possible in the Fragment Scoped Viewmodel. The Fragment scoped viewmodel will be cleared when the fragment is destroyed. Reducing the overall memory footprint.
来源:https://stackoverflow.com/questions/53653157/manually-clearing-an-android-viewmodel