Reputation: 4292
A viewmodel lives and dies with an activity or the fragment it is attached to. This has certain ramifications that it is beyond me why isn't anyone asking about (if we get the Navigation architecture into the picture).
According to the latest android blogs and the way navigation framework works , we are recommended to go in the Single Activity Multiple Fragments verse.
Supposedly I have the following app design .
Activity A (Application Entry Point)
----------
Fragment (A) (Uses ViewModel AB)
Fragment (B) (Uses ViewModel AB)
Fragment (C) (Uses ViewModel CDE)
Fragment (D) (Uses ViewModel CDE)
Fragment (E) (Uses ViewModel CDE)
Now since I use shared viewmodels that means my viewmodels would be attached to the activity. However this appears to be leaky. Like if I have traversed all the way from A to E and now come back popping off fragments to fragment B , the viewmodel CDE should be destroyed , but it wont be since it is connected to the activity.
Also we cannot connect our viewmodels to the fragment since we are going to be sharing their data.
The fact that only I am raising this question makes me believe i am at mistake here with my understanding. Would be elated if I could be given a proper insight into the situation.
Upvotes: 34
Views: 20879
Reputation: 6681
The only solution I found to get the view model from a fragment opened with navigation controller is to do
private val myViewModel by lazy {
requireParentFragment().childFragmentManager.primaryNavigationFragment?.getViewModel<MyViewModel>()
}
primaryNavigationFragment is used to get the currently displayed fragment inside the navigation (from this link)
Upvotes: 0
Reputation: 614
Since Navigation 2.1.0-alpha02
(stable in 2.1.0
), you can create ViewModels with a scope at a level of navigation graph through by navGraphViewModels()
.
To get a ViewModel not to be attached to an activity or a single fragment, you have to create a nested navigation graph and request instances of the ViewModel in the scope of that graph. This will cause that while you are inside the nested navigation graph, ViewModel will live and the fragments inside the nested graph will reuse the same instance of the ViewModel.
In this way, you can have several nested navigation graphs, each with a single instance of ViewModel that will be shared among the fragments that make up that graph.
I will follow your same distribution of fragments and ViewModels:
MainActivity (Application Entry Point)
----------
Fragment (A) (Uses SharedViewModelOne) -> navGraphOne
Fragment (B) (Uses SharedViewModelOne) -> navGraphOne
Fragment (C) (Uses SharedViewModelTwo) -> navGraphTwo
Fragment (D) (Uses SharedViewModelTwo) -> navGraphTwo
To achieve this you must follow these steps:
Your build.gradle(Module) should look like this
...
apply plugin: 'kotlin-kapt'
android {
...
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies{
...
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
kapt 'androidx.lifecycle:lifecycle-compiler:2.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.2.1'
}
Select the fragments that will share the same ViewModel and add them to a nested navigation graph.
To do this, select the fragments in your navigation graph designer,
then right click on them and choose Move to Nested Graph
In this example I added FragmentA and Fragment B to navGraphOne and FragmentC and
Fragment D to navGraphTwo.
Find more information about Nested Navigation Graph here
In Fragment A and Fragment B, request an instance of SharedViewModelOne.
private val modelOne: SharedViewModelOne by navGraphViewModels(R.id.navGraphOne) {
//defaultViewModelProviderFactory or the ViewModelProvider.Factory you are using.
defaultViewModelProviderFactory
}
override fun onCreateView(
..
): View? {
...
//use binding.lifecycleOwner = viewLifecycleOwner
//to make sure the observer disappears when the fragment is destroyed
modelOne.item.observe(viewLifecycleOwner, Observer {
//do Something
})
...
}
In Fragment C and Fragment D, request an instance of SharedViewModelTwo.
private val modelTwo: SharedViewModelTwo by navGraphViewModels(R.id.navGraphTwo) {
//defaultViewModelProviderFactory or the ViewModelProvider.Factory you are using.
defaultViewModelProviderFactory
}
override fun onCreateView(
..
): View? {
...
//use binding.lifecycleOwner = viewLifecycleOwner
//to make sure the observer disappears when the fragment is destroyed
modelTwo.item.observe(viewLifecycleOwner, Observer {
//do Something
})
...
}
Then to verify that only a single instance of the ViewModels is created and that it is shared among the fragments, override the onCleared()
method and add a checkpoint in the init{}
of the ViewModel.
For Example:
class SharedViewModelOne : ViewModel() {
private val _item = MutableLiveData<String>()
val item : LiveData<String>
get() = _item
init {
Log.d(TAG, "SharedViewModelOne has created!")
}
override fun onCleared() {
super.onCleared()
Log.d(TAG, "SharedViewModelOne has removed!")
}
}
After having followed the previous steps, you should be able to create a ViewModel that will be shared among the fragments that belong to the same nested navigation graph said ViewModel will only live while you are inside the graph, if you leave it, it will be destroyed.
If you feel that something is not very clear to you, you can review this repo and clarify your doubts.
Upvotes: 15
Reputation: 7459
Alternatively, if you are unable to perform the suggested solutions, you can simply clear the ViewModel
at the Activity
level where the shared ViewModel
is scoped.
You can do so by doing this:
getActivity().getViewModelStore().clear();
This will ensure that the shared viewmodel has its data cleared.
Upvotes: 0
Reputation: 1033
This is really a problem and has been reported to Google.
Fortunately since Navigation 2.1.0-alpha02
(stable in 2.1.0
) this problem has been solved. You can find the change log here and the document.
You can now create ViewModels that are scoped at a navigation graph level via the
by navGraphViewModels()
property delegate for Kotlin users or by using thegetViewModelStore()
API added toNavController
.
First you should select some of fragments in your nav graph designer, then right click on them and choice Move to Nested Graph
to create a new graph which will be used as a 'scope' like this:
class DetailFr : Fragment() {
private val vm: DetailViewModel by navGraphViewModels(R.id.main_nav_graph)
}
You can learn more about Nested Graph
here.
Upvotes: 33
Reputation: 725
Every LifecycleOwner (i.e. a Fragment or an Activity) keeps its models in a ViewModelStore that has a clear()
function. However, the clearing sweeps all the models from the ViewModelStore which in your case is undesirable (Both ViewModel AB and ViewModel CDE would be cleared form the Activity's ViewModelStore). 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 = [email protected]<VIEWMODEL>()
return ViewModelProvider(this.getIndividualViewModelStore<VIEWMODEL>(), factory).get(VIEWMODEL::class.java)
}
val viewModelStore = [email protected]<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 viewModelCDE : ViewModelCDE = (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<ViewModelCDE>()
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 = [email protected]
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 ${[email protected]}:"
)
for ((index, viewModelClass) in individualModelStores.keys.withIndex()) {
Log.w(
tag,
"${index + 1}) $viewModelClass\n"
)
}
}
}
}
}
Upvotes: 0
Reputation: 1590
I assumed this is your issue:
Like if I have traversed all the way from A to E and now come back popping off fragments to fragment B , the viewmodel CDE should be destroyed , but it wont be since it is connected to the activity.
You wanted to Share Data through between Multiple Fragments using ViewModel, yet you want to ensure the ViewModel's Data would be Destroy when the Fragment Navigate to certain Screen.
My Suggestion Solution for this is:
Create a Destroy Data Function within the ViewModel Class that will Destroy the ViewModel's Data by Overwrite its value to empty value such as ""
class CDEViewModel : ViewModel() {
var dataString: String = ""
fun destroyViewModelData() { // Function that will Destroythe Data
dataString= ""
}
}
Now you can Call the destroyViewModelData function in your Fragment whenever you need to make sure the ViewModel Data is being Clear/Destroy
class FragmentE {
private lateinit var cdeViewModel : CDEViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize your ViewModel
cdeViewModel = ViewModelProviders.of(this).get(CDEViewModel ::class.java)
}
override fun onStart() {
super.onStart()
// Set your Value Here
cdeViewModel.dataString = "String 1"
}
override fun onStop() {
super.onStop()
// Reset/Destroy Data when Screen is Being Close/Navigate to other Screen
// After Call this function, in Whatever Screen, the ViewModel previous Set ""String 1"" Data is Clear/Destroy and become "" empty value.
cdeViewModel.destroyViewModelData()
}
}
In your case, You can call destroyViewModelData function at onStop() of FragmentE, so when you have navigate from FragmentE to FragmentB, the CDEViewModel's data are all become "" empty String which mean it have been Reset/Destroy.
Hope this Simple Solution could help. Thank you.
Upvotes: 1