Search code examples
androiddaggerandroid-viewmodelandroid-jetpack-navigationandroid-navigation-graph

How to use a shared ViewModel, and avoid reusing the same instance of it every time with Navigation Component


My app consists of one single Activity with multiple Fragments, following the "single Activity app model", so that I can implement properly navigation using the Navigation Component in Android jetpack.

Most of my screens (Fragments) are standalone and don't depend on each other, hence they use their own ViewModel

Some features require navigation involving more than one Fragment. As those features share data between them that are passed back and forth through the Fragments, I use a shared ViewModel (as recommended by Google). I need to use the same instance of the shared ViewModel in all the associated Fragments, as I need the Fragments share the state of the shared ViewModel.

To use the same instance of the ViewModel in these associated Fragments, I need to create the ViewModel using the parent Activity (not the Fragment) when getting the ViewModel from the ViewModelProviders:

val viewModel = ViewModelProviders.of(
            parentActivity, factory.create()
        ).get(SharedViewModel::class.java)

This works, however, It produces one problem: when navigating a consecutive times to the first Fragment that requires the shared ViewModel, ViewModelProviders.of() will return the same instance of the ViewModel as before: The ViewModel is being shared between the Fragments, but also between different navigations to the feature implemented like this.

I understand why this is happening (Android is storing the ViewModel in a map, which is being used when requesting the ViewModel with ViewModelProviders.of()), but I don't know how I am expected to implement the "shared ViewModel pattern" properly.

The only workarounds I see are:

  • Create a different Activity for the feature that uses the Fragment with a shared ViewModel
  • Use nested Fragments, and use common parent Fragment for the feature that uses the Fragment with a shared ViewModel

With these two options, I would be able to create a ViewModel that will be shared between the Fragments intervening in the feature, and will be different each time I navigate to the feature.

The problem I see here is that this seems to be that against the fundamentals of the Navigation Component and the single Activity app. Each feature implemented this way will need to have a different navigation graph, as they will use a different navigation host. This would prevent me from using some of the nice features of Navigation Component.

What is the proper way to implement what I want? Am I missing anything, or is it the way it is?

Before Navigation Component I would use different Activities and Fragments and use Dagger scopes associated with the Activity/Fragment to achieve this. But I'm not sure what's the best way of implementing this with just one Activity`


Solution

  • I have discovered this can be done starting with 2.1.0-alpha02

    From: https://developer.android.com/jetpack/androidx/releases/navigation#2.1.0-alpha02

    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 the getViewModelStore() API added to NavController. b/111614463

    Basically:

    1. in the nav graph editor, create a nested graph, assigning one id to it
    2. when providing the ViewModel, do not do it from the Activity. Instead, use the navGraphViewModels extension function of Fragment.

    Example:

    Nested graph in the nav graph

    <navigation
            android:id="@+id/feature_nested_graph"
            android:label="Feature"
            app:startDestination="@id/firstFragment">
            <argument
                android:name="item_id"
                app:argType="integer" />
            <fragment
                android:id="@+id/firstFragment"
                [....]
            </fragment>
            [....]
        </navigation>
    

    For getting the ViewModel scoped to feature_nested_graph nested nav gaph:

            val viewModel: SharedViewModel
                    by fragment.navGraphViewModels(R.id.feature_nested_graph)
    
    

    or, if are injecting into the ViewModel and you are using a custom factory for that:

            val viewModel: SharedViewModel
                    by fragment.navGraphViewModels(R.id.feature_nested_graph) { factory2.create(assessmentId) }