I'm starting to learn about shared view model. Currently I've 3 fragments inside activity, 2 of them are inside nested navGraph.
I want to create shared navGraph viewModel scope to both of them, but I can't understand how and where I can initilaze the view model inside those fragments.
At all my past apps, I created Global view model
private lateinit var viewModel: MainViewModel
And then inside onCreateView
I initlaize the viewModel like this -
viewModel = ViewModelProvider(this, Factory(requireActivity().application)).get(
MainViewModel::class.java)
How can I do the same with navGraph viewModel scope, If I want to share one view model with 2 fragments?
Currently I've this approach:
private val homeViewModel: HomeViewModel by navGraphViewModels(R.id.nested_navigation)
And It's work, but
A. I never saw viewModel intilazied right in the global variable
B. I can't pass variables inside factory with this approach
You can create nav graph scoped viewmodels in Java by first getting the viewmodel store. A viewmodel store is basically just a container that stores viewmodels. Then get the viewModel by passing the viewModelStore into a viewmodelProvider. See, I told you it was super simple.
Scoping ViewModels to a navigation graph: You can now create ViewModels that are scoped at the navigation graph level using the by navGraphViewModels () property delegate for Kotlin users using the -ktx libraries or by using the getViewModelStoreOwner () API added to NavController.
B.) there's truth to this, you can definitely use a custom factory with navGraphViewModels, but what you really want (probably) is to implicitly pass any of your Fragment arguments to the ViewModel (note that both of your fragments must have the right keys in their arguments for this to work safely) by using a SavedStateHandle.
3a For Kotlin users, there is a sweet new extension function called navGraphViewModels. navGraphViewModels is a kotlin extension function for fragments that creates nav graph scoped view models. It accepts two parameters: a nav graph ID and also a viewmodel factory. Val order selection by navGraphViewModels (R.id.order_selection_nav_graph).
private val homeViewModel: HomeViewModel by navGraphViewModels(R.id.nested_navigation)
And It's work, but
A. I never saw viewModel initialized right in the global variable
B. I can't pass variables inside factory with this approach
A.) ViewModel in this case is initialized on first access, so if you just type homeViewModel
in onCreate
or onViewCreated
then it will be created with the right scope.
B.) there's truth to this, you can definitely use a custom factory with navGraphViewModels
, but what you really want (probably) is to implicitly pass any of your Fragment arguments to the ViewModel (note that both of your fragments must have the right keys in their arguments for this to work safely) by using a SavedStateHandle
.
To obtain a SavedStateHandle
, you need to use an AbstractSavedStateViewModelFactory
. To create one, you must create your ViewModel inside onViewCreated
(onCreate
won't work with navgraphs), which is easiest to do by using a ViewModelLazy
.
To create a viewModelLazy
, you can use createViewModelLazy
(what it says on the tin). This can define a way for you to pass in the ViewModelStoreOwner
(which is the NavBackStackEntry
), and the SavedStateRegistryOwner
(which is ALSO the NavBackStackEntry
).
So you can put this in your code and it should work.
inline fun <reified T : ViewModel> SavedStateRegistryOwner.createAbstractSavedStateViewModelFactory(
arguments: Bundle,
crossinline creator: (SavedStateHandle) -> T
): ViewModelProvider.Factory {
return object : AbstractSavedStateViewModelFactory(this, arguments) {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(
key: String, modelClass: Class<T>, handle: SavedStateHandle
): T = creator(handle) as T
}
}
inline fun <reified T : ViewModel> Fragment.navGraphSavedStateViewModels(
@IdRes navGraphId: Int,
crossinline creator: (SavedStateHandle) -> T
): Lazy<T> {
// Wrapped in lazy to not search the NavController each time we want the backStackEntry
val backStackEntry by lazy { findNavController().getBackStackEntry(navGraphId) }
return createViewModelLazy(T::class, storeProducer = {
backStackEntry.viewModelStore
}, factoryProducer = {
backStackEntry.createAbstractSavedStateViewModelFactory(
arguments = backStackEntry.arguments ?: Bundle(), creator = creator
)
})
}
inline fun <reified T : ViewModel> Fragment.fragmentSavedStateViewModels(
crossinline creator: (SavedStateHandle) -> T
): Lazy<T> {
return createViewModelLazy(T::class, storeProducer = {
viewModelStore
}, factoryProducer = {
createAbstractSavedStateViewModelFactory(arguments ?: Bundle(), creator)
})
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T : ViewModel> Fragment.fragmentViewModels(
crossinline creator: () -> T
): Lazy<T> {
return createViewModelLazy(T::class, storeProducer = {
viewModelStore
}, factoryProducer = {
object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(
modelClass: Class<T>
): T = creator.invoke() as T
}
})
}
Now you can do
private val homeViewModel: HomeViewModel by navGraphSavedStateViewModels(R.id.nested_navigation) { savedStateHandle ->
HomeViewModel(savedStateHandle)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view)
homeViewModel.someData.observe(viewLifecycleOwner) { someData ->
...
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With