Conditional navigation
Stay organized with collections Save and categorize content based on your preferences.

When designing navigation for your app, you might want to navigate to one destination versus another based on conditional logic. For example, a user might follow a deep link to a destination that requires the user to be logged in, or you might have different destinations in a game for when the player wins or loses.

User login

In this example, a user attempts to navigate to a profile screen that requires authentication. Because this action requires authentication, the user should be redirected to a login screen if they are not already authenticated.

The navigation graph for this example might look something like this:

a login flow is handled independently from the app's main navigation flow.
Figure 1. A login flow is handled independently from the app's main navigation flow.

To authenticate, the app must navigate to the login_fragment, where the user can enter a username and password to authenticate. If accepted, the user is sent back to the profile_fragment screen. If not accepted, the user is informed that their credentials are invalid using a Snackbar. If the user navigates back to the profile screen without logging in, they are sent to the main_fragment screen.

Here's the navigation graph for this app:

<navigationxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/main_fragment">
<fragment
android:id="@+id/main_fragment"
android:name="com.google.android.conditionalnav.MainFragment"
android:label="fragment_main"
tools:layout="@layout/fragment_main">
<action
android:id="@+id/navigate_to_profile_fragment"
app:destination="@id/profile_fragment"/>
</fragment>
<fragment
android:id="@+id/login_fragment"
android:name="com.google.android.conditionalnav.LoginFragment"
android:label="login_fragment"
tools:layout="@layout/login_fragment"/>
<fragment
android:id="@+id/profile_fragment"
android:name="com.google.android.conditionalnav.ProfileFragment"
android:label="fragment_profile"
tools:layout="@layout/fragment_profile"/>
</navigation>

MainFragment contains a button that the user can click to view their profile. If the user wants to see the profile screen, they must first authenticate. This interaction is modeled using two separate fragments, but it depends on shared user state. This state information is not the responsibility of either of these two fragments and is more appropriately held in a shared UserViewModel. This ViewModel is shared between the fragments by scoping it to the activity, which implements ViewModelStoreOwner. In the following example, requireActivity() resolves to MainActivity, because MainActivity hosts ProfileFragment:

Kotlin

classProfileFragment:Fragment(){
privatevaluserViewModel:UserViewModelbyactivityViewModels()
...
}

Java

publicclass ProfileFragmentextendsFragment{
privateUserViewModeluserViewModel;
@Override
publicvoidonViewCreated(@NonNullViewview,@NullableBundlesavedInstanceState){
super.onViewCreated(view,savedInstanceState);
userViewModel=newViewModelProvider(requireActivity()).get(UserViewModel.class);
...
}
...
}

The user data in UserViewModel is exposed via LiveData, so to decide where to navigate, you should observe this data. Upon navigating to ProfileFragment, the app shows a welcome message if the user data is present. If the user data is null, you then navigate to LoginFragment, since the user needs to authenticate before seeing their profile. Define the deciding logic in your ProfileFragment, as shown in the following example:

Kotlin

classProfileFragment:Fragment(){
privatevaluserViewModel:UserViewModelbyactivityViewModels()
overridefunonViewCreated(view:View,savedInstanceState:Bundle?){
super.onViewCreated(view,savedInstanceState)
valnavController=findNavController()
userViewModel.user.observe(viewLifecycleOwner,Observer{user->
if(user!=null){
showWelcomeMessage()
}else{
navController.navigate(R.id.login_fragment)
}
})
}
privatefunshowWelcomeMessage(){
...
}
}

Java

publicclass ProfileFragmentextendsFragment{
privateUserViewModeluserViewModel;
@Override
publicvoidonViewCreated(@NonNullViewview,@NullableBundlesavedInstanceState){
super.onViewCreated(view,savedInstanceState);
userViewModel=newViewModelProvider(requireActivity()).get(UserViewModel.class);
finalNavControllernavController=Navigation.findNavController(view);
userViewModel.user.observe(getViewLifecycleOwner(),(Observer<User>)user->{
if(user!=null){
showWelcomeMessage();
}else{
navController.navigate(R.id.login_fragment);
}
});
}
privatevoidshowWelcomeMessage(){
...
}
}

If the user data is null when they reach the ProfileFragment, they are redirected to the LoginFragment.

You can use NavController.getPreviousBackStackEntry() to retrieve the NavBackStackEntry for the previous destination, which encapsulates the NavController-specific state for the destination. LoginFragment uses the SavedStateHandle of the previous NavBackStackEntry to set an initial value indicating whether the user has successfully logged in. This is the state we would want to return if the user were to immediately press the system back button. Setting this state using SavedStateHandle ensures that the state persists through process death.

Kotlin

classLoginFragment:Fragment(){
companionobject{
constvalLOGIN_SUCCESSFUL:String="LOGIN_SUCCESSFUL"
}
privatevaluserViewModel:UserViewModelbyactivityViewModels()
privatelateinitvarsavedStateHandle:SavedStateHandle
overridefunonViewCreated(view:View,savedInstanceState:Bundle?){
savedStateHandle=findNavController().previousBackStackEntry!!.savedStateHandle
savedStateHandle.set(LOGIN_SUCCESSFUL,false)
}
}

Java

publicclass LoginFragmentextendsFragment{
publicstaticStringLOGIN_SUCCESSFUL="LOGIN_SUCCESSFUL"
privateUserViewModeluserViewModel;
privateSavedStateHandlesavedStateHandle;
@Override
publicvoidonViewCreated(@NonNullViewview,@NullableBundlesavedInstanceState){
userViewModel=newViewModelProvider(requireActivity()).get(UserViewModel.class);
savedStateHandle=Navigation.findNavController(view)
.getPreviousBackStackEntry()
.getSavedStateHandle();
savedStateHandle.set(LOGIN_SUCCESSFUL,false);
}
}

Once the user enters a username and password, they are passed to the UserViewModel for authentication. If authentication is successful, the UserViewModel stores the user data. The LoginFragment then updates the LOGIN_SUCCESSFUL value on the SavedStateHandle and pops itself off of the back stack.

Kotlin

classLoginFragment:Fragment(){
companionobject{
constvalLOGIN_SUCCESSFUL:String="LOGIN_SUCCESSFUL"
}
privatevaluserViewModel:UserViewModelbyactivityViewModels()
privatelateinitvarsavedStateHandle:SavedStateHandle
overridefunonViewCreated(view:View,savedInstanceState:Bundle?){
savedStateHandle=findNavController().previousBackStackEntry!!.savedStateHandle
savedStateHandle.set(LOGIN_SUCCESSFUL,false)
valusernameEditText=view.findViewById(R.id.username_edit_text)
valpasswordEditText=view.findViewById(R.id.password_edit_text)
valloginButton=view.findViewById(R.id.login_button)
loginButton.setOnClickListener{
valusername=usernameEditText.text.toString()
valpassword=passwordEditText.text.toString()
login(username,password)
}
}
funlogin(username:String,password:String){
userViewModel.login(username,password).observe(viewLifecycleOwner,Observer{result->
if(result.success){
savedStateHandle.set(LOGIN_SUCCESSFUL,true)
findNavController().popBackStack()
}else{
showErrorMessage()
}
})
}
funshowErrorMessage(){
// Display a snackbar error message
}
}

Java

publicclass LoginFragmentextendsFragment{
publicstaticStringLOGIN_SUCCESSFUL="LOGIN_SUCCESSFUL"
privateUserViewModeluserViewModel;
privateSavedStateHandlesavedStateHandle;
@Override
publicvoidonViewCreated(@NonNullViewview,@NullableBundlesavedInstanceState){
userViewModel=newViewModelProvider(requireActivity()).get(UserViewModel.class);
savedStateHandle=Navigation.findNavController(view)
.getPreviousBackStackEntry()
.getSavedStateHandle();
savedStateHandle.set(LOGIN_SUCCESSFUL,false);
EditTextusernameEditText=view.findViewById(R.id.username_edit_text);
EditTextpasswordEditText=view.findViewById(R.id.password_edit_text);
ButtonloginButton=view.findViewById(R.id.login_button);
loginButton.setOnClickListener(v->{
Stringusername=usernameEditText.getText().toString();
Stringpassword=passwordEditText.getText().toString();
login(username,password);
});
}
privatevoidlogin(Stringusername,Stringpassword){
userViewModel.login(username,password).observe(viewLifecycleOwner,(Observer<LoginResult>)result->{
if(result.success){
savedStateHandle.set(LOGIN_SUCCESSFUL,true);
NavHostFragment.findNavController(this).popBackStack();
}else{
showErrorMessage();
}
});
}
privatevoidshowErrorMessage(){
// Display a snackbar error message
}
}

Note that all logic pertaining to authentication is held within UserViewModel. This is important, as it is not the responsibility of either LoginFragment or ProfileFragment to determine how users are authenticated. Encapsulating your logic in a ViewModel makes it not only easier to share but also easier to test. If your navigation logic is complex, you should especially verify this logic through testing. See the Guide to app architecture for more information on structuring your app’s architecture around testable components.

Back in the ProfileFragment, the LOGIN_SUCCESSFUL value stored in the SavedStateHandle can be observed in the onCreate() method. When the user returns to the ProfileFragment, the LOGIN_SUCCESSFUL value will be checked. If the value is false, the user can be redirected back to the MainFragment.

Kotlin

classProfileFragment:Fragment(){
...
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
valnavController=findNavController()
valcurrentBackStackEntry=navController.currentBackStackEntry!!
valsavedStateHandle=currentBackStackEntry.savedStateHandle
savedStateHandle.getLiveData<Boolean>(LoginFragment.LOGIN_SUCCESSFUL)
.observe(currentBackStackEntry,Observer{success->
if(!success){
valstartDestination=navController.graph.startDestination
valnavOptions=NavOptions.Builder()
.setPopUpTo(startDestination,true)
.build()
navController.navigate(startDestination,null,navOptions)
}
})
}
...
}

Java

publicclass ProfileFragmentextendsFragment{
...
@Override
publicvoidonCreate(@NullableBundlesavedInstanceState){
super.onCreate(savedInstanceState);
NavControllernavController=NavHostFragment.findNavController(this);
NavBackStackEntrynavBackStackEntry=navController.getCurrentBackStackEntry();
SavedStateHandlesavedStateHandle=navBackStackEntry.getSavedStateHandle();
savedStateHandle.getLiveData(LoginFragment.LOGIN_SUCCESSFUL)
.observe(navBackStackEntry,(Observer<Boolean>)success->{
if(!success){
intstartDestination=navController.getGraph().getStartDestination();
NavOptionsnavOptions=newNavOptions.Builder()
.setPopUpTo(startDestination,true)
.build();
navController.navigate(startDestination,null,navOptions);
}
});
}
...
}

If the user successfully logged in, the ProfileFragment displays a welcome message.

The technique used here of checking the result allows you to distinguish between two different cases:

  • The initial case, where the user is not logged in and should be asked to login.
  • The user is not logged in because they chose not to login (a result of false).

By distinguishing these use cases, you can avoid repeatedly asking the user to login. The business logic for handling failure cases is left to you and might include displaying an overlay that explains why the user needs to login, finishing the entire activity, or redirecting the user to a destination that does not require login, as was the case in the previous code example.

Content and code samples on this page are subject to the licenses described in the Content License. Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.

Last updated 2025年02月10日 UTC.