Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

injecting viewmodel with navigation-graph scope: NavController is not available before onCreate()

I'm using a navigation-component in my application and also using shared ViewModel between multiple fragments that are in the same graph. Now I want to instantiate the ViewModel with this graph scope with this.

As you know, in fragments we should inject objects ( ViewModel,..etc ) in onAttach:

but when I want to do this (injecting ViewModel with a graph scope in onAttach), this error occurs:

IllegalStateException: NavController is not available before onCreate()

Do you know how I can do this?

like image 863
beigirad Avatar asked Dec 18 '19 08:12

beigirad


2 Answers

In short, you could provide the ViewModel lazily with dagger Provider or Lazy.

The long explanation is:

Your injections points are correct. According to https://dagger.dev/android#when-to-inject

DaggerActivity calls AndroidInjection.inject() immediately in onCreate(), before calling super.onCreate(), and DaggerFragment does the same in onAttach().

The problem is some kind of race condition between when Android recreates the Activity and the Fragments attached to the FragmentManger and when the NavController can be provided. More specifically:

  1. one Activity that has Fragments attached is destroyed by the OS (can be reproduced with "don't keep Activities" from "developer settings")
  2. user navigates back to the Activity, OS proceeds to recreate the Activity
  3. Activity calls setContentView while being recreated.
  4. This causes the Fragments in the FragmentManager to be reattached, which involve calling Fragment#onAttach
  5. The Fragment is injected in Fragment#onAttach
  6. Dagger tries to provide the NavController

BUT you cannot get the NavController from the Activity by this point, as Activity#onCreate has not finished yet and you get

IllegalStateException: NavController is not available before onCreate()

The solution I found is to inject provide the NavCotroller or things that depend on the NavController (such as the ViewModel, because Android needs the NavController to get nav-scoped VideModels) lazily. This can be done in two ways:

  • with Lazy
  • with Provided

(REF: https://proandroiddev.com/dagger-2-part-three-new-possibilities-3daff12f7ebf)

ie: inject the ViewModel to the Fragment or implementation of navigator like this:

    @Inject
    lateinit var viewModel: Provider<ViewModel>

then use it like this:

viewModel.get().events.observe(this) {....}

Now, the ViewModel can by provided by Dagger like:


    @Provides
    fun provideViewModel(
        fragment: Fragment,
        argumentId: Int
    ): CreateMyViewModel {

        val viewModel: CreateMyViewModel
                by fragment.navGraphViewModels(R.id.nested_graph_id)

        return viewModel
    }

Dagger won't try to resolve the provisioning when the Fragment is injected, but when it's used, hence, the race condition will be solved.

I really hate not being able to use my viewModels directly and need to use Provider, but it's the only workaround I see to solve this issue, which I'm sure it was an oversight by Google (I don't blame them, as keeping track of the absurd lifecycle of Fragment and Activities is so difficult).

like image 95
GaRRaPeTa Avatar answered Oct 05 '22 10:10

GaRRaPeTa


...we should inject objects ( ViewModel,..etc ) in onAttach...

Looks like it is currently a no go for such injection with the original by navGraphViewModels(R.id.nav_graph) delegated property provided by androidx.navigation package because from the source code

findNavController().getBackStackEntry(navGraphId) and

public final NavController getNavController() it stated that:

 * Returns the {@link NavController navigation controller} for this navigation host.
 * This method will return null until this host fragment's {@link #onCreate(Bundle)}

And here are some workarounds:

https://github.com/InsertKoinIO/koin/issues/442

like image 41
CHAN Avatar answered Oct 05 '22 08:10

CHAN