Noam
Noam

Reputation: 535

How to initilaize viewModel using navGraph scope

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

Upvotes: 7

Views: 3134

Answers (1)

EpicPandaForce
EpicPandaForce

Reputation: 81569

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 ->
        ...
    }
}

Upvotes: 11

Related Questions