Conditional Navigation in Jetpack Compose

Francesc Vilarino Guell
7 min readFeb 19, 2022

In today’s story we will learn how to conditionally navigate to a screen in Jetpack Compose. This is the kind of scenario you might encounter if your app has an onboarding flow, which needs to be shown only once, or a login screen, which the user needs to complete before accessing the app’s main content.

In this story we will use onboarding as an example, but the principles discussed here apply to any conditional navigation in Jetpack Compose.

The sample app

Main screens

To keep things simple, we will use an app with 2 screens, a Home screen that displays the main content, and an Onboarding screen that could show some slides on how to use the app, for instance. In our case the Onboarding screen will simply contain a button that will signify, when clicked, that the user has completed the onboarding flow.

Let’s define the 2 screens that we will use in our app:

These 2 screen are very simple, the Home screen just shows a label to indicate we have onboarded, and the Onboarding screen shows a button that, when clicked, calls an onOnboarded lambda that will mark the user as onboarded.

Navigation

For our navigation we will use Jetpack Compose’s navigation library. When using conditional navigation, it is important that the conditional screens, in our case the Onboarding screen, are not the start destination in our navigation graph, and the main content, the Home screen, is instead the start destination. The correct way to configure the navigation graph is to set the main content as the start destination, perform a check to see if we need to navigate to onboarding (or login) and, if that’s the case, trigger a navigation event to that screen.

With that in mind, we can define our navigation graph as shown below:

As we can see above, we define 2 nodes in our navigation graph, the home node that will render the Home screen, and the onboarding node that will render the Onboarding screen. For now the lambda for Onboarding is left empty. As mentioned above, the Home screen is the start destination.

Ancillary classes

To support our onboarding flow we need to define a few ancillary classes that do not play a role in the conditional navigation, but are needed to wire things up properly. We will need to define a storage mechanism to persist whether the user has already onboarded (on a production app we would preferably use the Data Store library, or the legacy Shared Preferences), plus some enums for the state.

Let’s define these classes so we can get this out of the way:

So we defined:

  • OnboardingState which will represent whether the user has already completed the onboarding flow.
  • OnboardingResult which indicates, once we have navigated to the Onboarding screen, whether the user completed onboarding, or cancelled the flow.
  • A storage mechanism to persist the onboarding state. The implementation for this story is an in-memory storage solution, it is not persisted — in a production app this would be persisted using data store or shared preferences. This storage solution exposes the current onboarding state as a flow, so that it can be observed.

Navigating to Onboarding

Now that we have these ancillary classes out of the way, let’s see how we can do the conditional navigation to the Onboarding screen.

When we start the app, the Home screen is the start destination, so that gets displayed on app launch. Here we will need to check the onboarding status from our storage, to determine whether the onboarding flow has already been completed, in which case we will show the home content. If the onboarding flow is still pending, then we need to navigate to the Onboarding screen.

Let’s update our Home screen to check the current onboarding state and navigate to the Onboarding screen if it has not been shown yet, the updates are shown below:

Let’s go over the changes we need to do on the Home screen:

  1. First we update our Home node in the navigation graph to pass to the HomeRoute a reference to our storage class, and a lambda to call in order to navigate to the Onboarding Screen.
  2. We define a HomeRoute composable that wraps the HomeScreen, and accepts the OnboardingStorage class.
  3. Our HomeRoute also takes a lambda to navigate to Onboarding.
  4. In the HomeRoute we observe the onboarding state from the OnboardingStorage class.
  5. Based on the onboarding state, we will do a couple of different things.
  6. If the user has not onboarded, we navigate to Onboarding.
  7. Note that navigating to Onboarding is within a LaunchedEffect — this is important because any side effects, i.e., actions not directly related to composition, should not be done during the composition flow, but as part of a side effect, in this case we use LaunchedEffect because we want to this just once.
  8. If the user has Onboarded, then we display the Home screen content.

With this in place, when we launch the app, it navigates immediately to Onboarding, so we have our conditional navigation in place.

Handling onboarding result

Next we need to handle the onboarding result, either completed, so that we can show the home content, or cancelled. In our case we will close the app if the user cancels the onboarding flow, but you could do something else based on your requirements.

To pass the onboarding result from the Onboarding screen back to the Home screen we will use the SavedStateHandle that is associated with each navigation node in the navigation graph. This is a bundle, i.e., a set of key/value pairs that can be used to propagate information between screens.

The SavedStateHandle has APIs to expose its values as a LiveData, and the Compose library offers extension functions on LiveData that allow us to observe these values within the composition, so we will be using that.

The way this is going to work is as follows:

  • When we launch the app, we will check in the Home screen if we need to show onboarding.
  • If we do, then we will check the SavedStateHandle for a result from the onboarding screen. This value will initially be null as it has not been populated yet, in which case we will navigate to the Onboarding screen. If the value is not null, then it could be OnboardingComplete or OnboardingCancelled. If it is OnboardingComplete, we will show the home content and, if it is OnboardingCancelled, we will close the app.
  • In the Onboarding screen, when it first launches, we will set the Onboarding result as OnboardingCancelled — that will cover the case of the user exiting the screen via the back button and returning to Home. Once the user has completed the onboarding flow, we will set the onboarding result as OnboardingCompleted and pop the back stack, to return to Home.
  • When we return to the Home screen, we will retrieve the onboarding result from the SavedStateHandle and either show the main content or exit the app.

Let’s see the code changes we need to do in order to implement this flow, starting with the navigation graph:

  1. In our Home Route we provide the Storage class to observe the onboarding state.
  2. We also provide the SavedStateHandle to retrieve the result from the Onboarding screen.
  3. We pass a lambda to navigate to the Onboarding screen.
  4. And also we pass a lambda to finish the app.
  5. On the Onboarding node we first initialize the previous back stack entry’s SavedStateHandle (belonging to the Home navigation graph node) with a result of OnboardingResult.Cancelled, this way if the user leaves the Onboarding screen this will be the default value read when returning to the Home screen.
  6. We also provide a lambda to pop the backstack, for when the user has completed onboarding.

Now that we have the updated navigation in place, let’s see the changes we need in the Home screen:

  1. We observe the onboarding result from the SavedStateHandle that we receive as argument.
  2. If the user has not been shown onboarding yet, we check the onboarding result.
  3. If it is null it means it is a fresh app launch, so we navigate the user to the Onboarding screen.
  4. If the value is Completed then the Onboarding flow was completed successfully and we show the main content. This should not be hit because we are setting the onboarding state to completed in the onboarding screen, so when returning to home we fall to the OnboardingState.Onboarded clause, but it is added here for completeness sake.
  5. Finally, if the value is Cancelled it means the user cancelled the onboarding flow and we call the lambda to exit the app.

And finally the updates to the Onboarding screen:

  1. The Onboarding screen takes the onboarding storage, to update the state, and a lambda to pop the back stack.
  2. When we display the onboarding content we provide a callback for when onboarding has completed.
  3. In this callback we update our storage to persist that the user has completed the onboarding flow.
  4. And use the lambda to pop the back stack and return to the Home screen.

Conclusion

This story shows how to conditionally navigate to a screen in Jetpack Compose, how to pass data back from a screen using the SavedStateHandle available in the backstack entries of the navigation graph and how to read that result when returning to the originating screen. The principles showed here can be used for login screens as well as any other screen that needs to be displayed only under certain circumstances.

I hope you found this useful, the full sample project is available here.

--

--