Jetpack Navigation Global/Shared Graph State

The Short Version

If you want to pass a value into a nav-graph and make it available to every destination in the graph, without having to pass it from destination to destination as an argument, take a look at the graph-scoped view model that was added to the Android Navigation Component in version 2.2.0. You can pass the value into the first destination as an argument, then have it store the argument on the shared view model. Any destination in the graph can then access it by declaring its own graph-scoped view model property, something like this, where MyViewModel is a class you create, which must extend ViewModel:

private val graphViewModel: MyViewModel by navGraphViewModels(R.id.my_nav_graph)

The Longer Version

I recently encountered a bug where destinations several steps into a login nav-graph needed to know whether -- after the user successfully logs into their account -- the next action in the graph should be taken, or the graph should be exited. In most scenarios, we want to follow up with a screen that encourages the user to flesh out their account details. However, if the login graph was initiated from an onboarding component that has its own UI for onboarding follow-up, we were showing a "Find Friends" screen twice: once as part of the login graph, and then again in the onboarding component.

Being new to Jetpack Navigation, it took me a while to understand my options for getting the "followup" flag to the two behavioral switches in the graph where decide whether we should keep going or exit the nav-graph. My first thought was to use arguments, and pass them via Bundle from destination to destination. Then I learned that the safe-args Gradle plugin makes this less bug-prone, via strongly typed arguments made available to destinations via generated classes. While this seemed a cumbersome approach, forcing several destinations to pass along an argument they ideally wouldn't even know about, the documentation didn't suggest a better alternative. I went ahead and implemented this, and it worked... but I decided to look again for a better way.

Although the main documentation on how to pass data between destinations doesn't mention the graph-scoped view model, it's a super useful feature of Navigation. I didn't see a way to access the view-model from the Activity that calls NavController.setGraph(), so I passed the follow-up flag into the first destination as an argument, which is then responsible for assigning it to the corresponding property on the graph-scoped view model.

Here's what I ended up with. The value is passed into my LoginActivity via a standard Intent argument, and within the Activity's onCreate() method I have this method to initialize the login graph:

private fun initNavGraph(skipOnboardingFollowup: Boolean) {
val args = LoginChooserFragmentArgs.Builder()
.setSkipOnboardingFollowup(skipOnboardingFollowup)
.build()
navController.setGraph(R.navigation.account_login_nav_graph, args.toBundle())
}

In the above method, LoginChooserFragmentArgs is a generated class. It exists because I added an argument to my login-chooser destination:

<fragment
android:id="@+id/account_login_chooser"
android:name="com.example.account.login.chooser.LoginChooserFragment"
tools:layout="@layout/fragment_signin_choice">
<action
android:id="@+id/action_login_chooser_to_login_phone"
app:destination="@id/account_login_phone" />
<argument
android:name="skipOnboardingFollowup"
app:argType="boolean"
android:defaultValue="false" />
</fragment>

Within LoginChooserFragment, I then have two properties: one for the LoginChooserFragmentArgs, so I can retrieve the value that was passed in; and another for the graph-scoped view model:

private val navArgs: LoginChooserFragmentArgs by navArgs()
private val graphViewModel: AccountLoginGraphViewModel by navGraphViewModels(R.id.account_login_nav_graph)

Note that the graph ID, account_login_nav_graph, is defined on the root navigation element of my graph XML file. That's the key to properly scoping the view model.

I then update my view model with the arg value:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
graphViewModel.skipOnboardingFollowup = navArgs.skipOnboardingFollowup
...
}

Now, from any destination in my graph, I can declare graphViewModel and populate it by navGraphViewModels(), as done above, and access this behavioral flag. Much tidier and less bug-prone than having to pass it along from destination to destination.

See an opportunity to improve on this? Mention it in comments below. Thanks!

P.S. Sorry about the code formatting. For some reason, gist embeds aren't working in this post.

comments powered by Disqus