Edit: This question is a bit out of date now that Google has given us the ability to scope ViewModel
to navigation graphs. The better approach (rather
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.
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"
)
}
}
}
}
}
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.
As OP and Archie said, Google has given us the ability to scope ViewModel to navigation graphs. I will add how to do it here if you are using the navigation component already.
You can select all the fragments that needs to be grouped together inside the nav graph and right-click->move to nested graph->new graph
now this will move the selected fragments to a nested graph inside the main nav graph like this:
<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>
Now, inside the fragments when you initialise the viewmodel do this
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)
if you need to pass the viewmodel factory(may be for injecting the viewmodel) you can do it like this:
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph) { viewModelFactory }
Make sure its R.id.checkout_graph
and not R.navigation.checkout_graph
For some reason creating the nav graph and using include
to nest it inside the main nav graph was not working for me. Probably is a bug.
Source: https://medium.com/androiddevelopers/viewmodels-with-saved-state-jetpack-navigation-data-binding-and-coroutines-df476b78144e
Thanks, OP and @Archie for pointing me in the right direction.
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.
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.
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:
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 a fragment
will ever be cleared. 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.