When designing navigation for your app, you might want to navigate to one destination versus another based on conditional logic. For example, you might have some destinations that require 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:

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.
Destinations in this app are represented using fragments that are hosted by a single activity.
Here's the navigation graph for this app:
<navigation xmlns: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 if they want 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 a shared state—whether the user is authenticated, and if so, the
authenticated user name. Note that this state information is not the
responsibility of either of these two fragments and is more appropriately held
in a shared ViewModel, as shown in the following example:
Kotlin
class LoginViewModel : ViewModel() {
enum class AuthenticationState {
UNAUTHENTICATED, // Initial state, the user needs to authenticate
AUTHENTICATED , // The user has authenticated successfully
INVALID_AUTHENTICATION // Authentication failed
}
val authenticationState = MutableLiveData<AuthenticationState>()
var username: String
init {
// In this example, the user is always unauthenticated when MainActivity is launched
authenticationState.value = AuthenticationState.UNAUTHENTICATED
username = ""
}
fun refuseAuthentication() {
authenticationState.value = AuthenticationState.UNAUTHENTICATED
}
fun authenticate(username: String, password: String) {
if (passwordIsValidForUsername(username, password)) {
this.username = username
authenticationState.value = AuthenticationState.AUTHENTICATED
} else {
authenticationState.value = AuthenticationState.INVALID_AUTHENTICATION
}
}
private fun passwordIsValidForUsername(username: String, password: String): Boolean {
...
}
}
Java
public class LoginViewModel extends ViewModel {
public enum AuthenticationState {
UNAUTHENTICATED, // Initial state, the user needs to authenticate
AUTHENTICATED, // The user has authenticated successfully
INVALID_AUTHENTICATION // Authentication failed
}
final MutableLiveData<AuthenticationState> authenticationState =
new MutableLiveData<>();
String username;
public LoginViewModel() {
// In this example, the user is always unauthenticated when MainActivity is launched
authenticationState.setValue(AuthenticationState.UNAUTHENTICATED);
username = "";
}
public void authenticate(String username, String password) {
if (passwordIsValidForUsername(username, password)) {
this.username = username;
authenticationState.setValue(AuthenticationState.AUTHENTICATED);
} else {
authenticationState.setValue(AuthenticationState.INVALID_AUTHENTICATION);
}
}
public void refuseAuthentication() {
authenticationState.setValue(AuthenticationState.UNAUTHENTICATED);
}
private boolean passwordIsValidForUsername(String username, String password) {
...
}
}
A ViewModel is scoped to a
ViewModelStoreOwner.
You can share data between the fragments by having a ViewModel scoped to the
activity, which implements ViewModelStoreOwner. In the following example,
requireActivity() resolves to MainActivity because MainActivity hosts
ProfileFragment:
Kotlin
class LoginFragment : Fragment() {
private val viewModel: LoginViewModel by activityViewModels()
...
}
Java
public class LoginFragment extends Fragment {
private LoginViewModel viewModel;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);
...
}
...
}
The user’s authentication state is represented as an enum class in
LoginViewModel and exposed via LiveData, so in order to decide where to
navigate, you should observe that state. Upon navigating to ProfileFragment,
the app shows a welcome message if the user is authenticated. If not
authenticated, then you navigate to LoginFragment, since the user needs to
authenticate before seeing their profile. You need to define the deciding logic
in your ViewModel, as shown in the following example:
Kotlin
class ProfileFragment : Fragment() {
private val viewModel: LoginViewModel by activityViewModels()
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
welcomeTextView = view.findViewById(R.id.welcome_text_view)
val navController = findNavController()
viewModel.authenticationState.observe(viewLifecycleOwner, Observer { authenticationState ->
when (authenticationState) {
AUTHENTICATED -> showWelcomeMessage()
UNAUTHENTICATED -> navController.navigate(R.id.login_fragment)
}
})
}
private fun showWelcomeMessage() {
...
}
}
...
Java
public class ProfileFragment extends Fragment {
private LoginViewModel viewModel;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);
welcomeTextView = view.findViewById(R.id.welcome_text_view);
final NavController navController = Navigation.findNavController(view);
viewModel.authenticationState.observe(getViewLifecycleOwner(),
new Observer<LoginViewModel.AuthenticationState>() {
@Override
public void onChanged(LoginViewModel.AuthenticationState authenticationState) {
switch (authenticationState) {
case AUTHENTICATED:
showWelcomeMessage();
break;
case UNAUTHENTICATED:
navController.navigate(R.id.loginFragment);
break;
}
}
});
}
private void showWelcomeMessage() {
...
}
...
}
If the user is not authenticated when they reach the ProfileFragment, they
navigate to the LoginFragment. Once there, they are able to enter a username
and password, which is then passed to the LoginViewModel.
If authentication is successful, then the ViewModel sets the authentication
state to AUTHENTICATED. This causes the LoginFragment to be popped off of
the back stack, taking the user back to the ProfileFragment. If authentication
is unsuccessful due to invalid credentials, the state is set to
INVALID_AUTHENTICATION, and the user is presented with a Snackbar in the
LoginFragment. Finally, if they press the Back button, the state is set to
UNAUTHENTICATED and the stack is popped back to the MainFragment.
Kotlin
class LoginFragment : Fragment() {
private val viewModel: LoginViewModel by activityViewModels()
private lateinit var usernameEditText: EditText
private lateinit var passwordEditText: EditText
private lateinit var loginButton: Button
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
usernameEditText = view.findViewById(R.id.username_edit_text)
passwordEditText = view.findViewById(R.id.password_edit_text)
loginButton = view.findViewById(R.id.login_button)
loginButton.setOnClickListener {
viewModel.authenticate(usernameEditText.text.toString(),
passwordEditText.text.toString())
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
viewModel.refuseAuthentication()
navController.popBackStack(R.id.main_fragment, false)
})
val navController = findNavController()
viewModel.authenticationState.observe(viewLifecycleOwner, Observer { authenticationState ->
when (authenticationState) {
AUTHENTICATED -> navController.popBackStack()
INVALID_AUTHENTICATION -> showErrorMessage()
}
})
}
private void showErrorMessage() {
...
}
}
Java
public class LoginFragment extends Fragment {
private LoginViewModel viewModel;
private EditText usernameEditText;
private EditText passwordEditText;
private Button loginButton;
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);
usernameEditText = view.findViewById(R.id.username_edit_text);
passwordEditText = view.findViewById(R.id.password_edit_text);
loginButton = view.findViewById(R.id.login_button);
loginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
viewModel.authenticate(usernameEditText.getText().toString(),
passwordEditText.getText().toString());
}
});
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(),
new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
viewModel.refuseAuthentication();
navController.popBackStack(R.id.main_fragment, false);
}
});
final NavController navController = Navigation.findNavController(view);
final View root = view;
viewModel.authenticationState.observe(getViewLifecycleOwner(),
new Observer<LoginViewModel.AuthenticationState>() {
@Override
public void onChanged(LoginViewModel.AuthenticationState authenticationState) {
switch (authenticationState) {
case AUTHENTICATED:
navController.popBackStack();
break;
case INVALID_AUTHENTICATION:
Snackbar.make(root,
R.string.invalid_credentials,
Snackbar.LENGTH_SHORT
).show();
break;
}
}
});
}
}
Note that all logic pertaining to authentication is held within
LoginViewModel. 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.
When the user returns to the ProfileFragment, their authentication state is
checked again. If they are now authenticated, the app displays a welcome message
using the authenticated username, as shown in the following example:
Kotlin
class ProfileFragment : Fragment() {
private val viewModel: LoginViewModel by activityViewModels()
private lateinit var welcomeTextView: TextView
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
welcomeTextView = view.findViewById(R.id.welcome_text_view)
val navController = findNavController()
viewModel.authenticationState.observe(viewLifeycleOwner, Observer { authenticationState ->
when (authenticationState) {
AUTHENTICATED -> showWelcomeMessage()
UNAUTHENTICATED -> navController.navigate(R.id.loginFragment)
})
}
private fun showWelcomeMessage() {
welcomeTextView.text = getString(R.string.welcome, viewModel.username)
}
...
}
Java
public class ProfileFragment extends Fragment {
private LoginViewModel viewModel;
private TextView welcomeTextView;
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);
welcomeTextView = view.findViewById(R.id.welcome_text_view);
final NavController navController = Navigation.findNavController(view);
viewModel.authenticationState.observe(getViewLifecycleOwner(),
new Observer<LoginViewModel.AuthenticationState>() {
@Override
public void onChanged(LoginViewModel.AuthenticationState authenticationState) {
switch (authenticationState) {
case AUTHENTICATED:
showWelcomeMessage();
break;
case UNAUTHENTICATED:
navController.navigate(R.id.loginFragment);
break;
}
}
});
}
private void showWelcomeMessage() {
welcomeTextView.setText(getString(R.string.welcome, viewModel.username));
}
...
}
Not every navigation action is based on conditions, but this pattern can be
quite useful for those that are. You determine how a user navigates through your
app by defining the conditions by which they navigate and providing a shared
source of truth in a ViewModel for communication between fragments.
First-time user experience
A first-time user experience (FTUE) is a specific flow that users see only when launching your app for the first time. Rather than make this flow part of your app's main navigation graph, you should keep this flow as a separate nested navigation graph.
Building onto the login example in the previous section, you might have a scenario where the user has a chance to register if they do not have a login, as shown with a REGISTER button in figure :

Figure 2: The login screen now contains a REGISTER button.
When the user clicks on the REGISTER button, they are taken to a sub-navigation flow specific to registration. After registering, the back stack is popped, and the user is taken directly into the profile screen.
The Navigation graph in the below example, has been updated to include a nested navigation graph. An action has also been added to the login_fragment and can trigger in response to tapping REGISTER:
<navigation xmlns: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/login_fragment"
android:name="com.google.android.conditionalnav.LoginFragment"
android:label="login_fragment"
tools:layout="@layout/fragment_login">
<action android:id="@+id/action_login_fragment_to_register_fragment"
app:destination="@id/registration_graph" />
</fragment>
<navigation android:id="@+id/registration_graph"
app:startDestination="@id/enter_user_profile_fragment">
<fragment android:id="@+id/enter_user_profile_fragment"
android:name="com.google.android.conditionalnav.registration.EnterProfileDataFragment"
android:label="Enter Profile Data"
tools:layout="@layout/fragment_enter_profile_info">
<action android:id="@+id/move_to_choose_user_password"
app:destination="@id/choose_user_password_fragment" />
</fragment>
<fragment android:id="@+id/choose_user_password_fragment"
android:name="com.google.android.conditionalnav.registration.ChooseUserPasswordFragment"
android:label="Choose User + Password"
tools:layout="@layout/fragment_choose_user_password" />
</navigation>
</navigation>
Represented visually in the Navigation editor, this nested graph appears as a smaller Nested Graph with the registration_graph id on top, as shown in figure 3:

Figure 3: The navigation graph now shows the nested registration_graph
Double-click on the Nested Graph in the editor to reveal the details of the registration graph. In figure 4, you can see a two-screen registration flow. The first screen gathers the user's full name and biographical information. The second screen captures their desired username and password. To go back to the main navigation graph, click ← Root in the Destinations pane.

Figure 4: The nested graph shows the registration flow.
The start destination of this nested navigation graph is the Enter Profile Data screen. Once the user enters profile data and clicks the NEXT button, the app navigates to the Create Login Credentials screen. Once they finish creating their username and password, they can click REGISTER + LOGIN to be taken directly into the profile screen.
As with the previous example, a ViewModel is used to share information between
the registration fragments:
Kotlin
class RegistrationViewModel : ViewModel() {
enum class RegistrationState {
COLLECT_PROFILE_DATA,
COLLECT_USER_PASSWORD,
REGISTRATION_COMPLETED
}
val registrationState =
MutableLiveData<RegistrationState>(RegistrationState.COLLECT_PROFILE_DATA)
// Simulation of real-world scenario, where an auth token may be provided as
// an alternate authentication mechanism instead of passing the password
// around. This is set at the end of the registration process.
var authToken = ""
private set
fun collectProfileData(name: String, bio: String) {
// ... validate and store data
// Change State to collecting username and password
registrationState.value = RegistrationState.COLLECT_USER_PASSWORD
}
fun createAccountAndLogin(username: String, password: String) {
// ... create account
// ... authenticate
this.authToken = // token
// Change State to registration completed
registrationState.value = RegistrationState.REGISTRATION_COMPLETED
}
fun userCancelledRegistration() : Boolean {
// Clear existing registration data
registrationState.value = RegistrationState.COLLECT_PROFILE_DATA
authToken = ""
return true
}
}
Java
public class RegistrationViewModel extends ViewModel {
enum RegistrationState {
COLLECT_PROFILE_DATA,
COLLECT_USER_PASSWORD,
REGISTRATION_COMPLETED
}
private MutableLiveData<RegistrationState> registrationState =
new MutableLiveData<>(RegistrationState.COLLECT_PROFILE_DATA);
public MutableLiveData<RegistrationState> getRegistrationState() {
return registrationState;
}
// Simulation of real-world scenario, where an auth token may be provided as
// an alternate authentication mechanism instead of passing the password
// around. This is set at the end of the registration process.
private String authToken;
public String getAuthToken() {
return authToken;
}
public void collectProfileData(String name, String bio) {
// ... validate and store data
// Change State to collecting username and password
registrationState.setValue( RegistrationState.COLLECT_USER_PASSWORD);
}
public void createAccountAndLogin(String username, String password) {
// ... create account
// ... authenticate
this.authToken = // token
// Change State to registration completed
registrationState.setValue(RegistrationState.REGISTRATION_COMPLETED);
}
public boolean userCancelledRegistration() {
// Clear existing registration data
registrationState.setValue(RegistrationState.COLLECT_PROFILE_DATA);
authToken = "";
return true;
}
}
The registration state of this ViewModel is observed from the fragments of
each registration screen. The state drives moving to the next screen and is
updated by RegistrationViewModel based on user interactions. Pressing back at
any time cancels the registration process and pops the user back to the login
screen:
Kotlin
class EnterProfileDataFragment : Fragment() {
val registrationViewModel by activityViewModels<RegistrationViewModel>()
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController()
...
// When the next button is clicked, collect the current values from the
// two edit texts and pass to the ViewModel to store.
view.findViewById<Button>(R.id.next_button).setOnClickListener {
val name = fullnameEditText.text.toString()
val bio = bioEditText.text.toString()
registrationViewModel.collectProfileData(name, bio)
}
// RegistrationViewModel will update the registrationState to
// COLLECT_USER_PASSWORD when ready to move to the choose username and
// password screen.
registrationViewModel.registrationState.observe(
viewLifecycleOwner, Observer { state ->
if (state == COLLECT_USER_PASSWORD) {
navController.navigate(R.id.move_to_choose_user_password)
}
})
// If the user presses back, cancel the user registration and pop back
// to the login fragment. Since this ViewModel is shared at the activity
// scope, its state must be reset so that it will be in the initial
// state if the user comes back to register later.
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
registrationViewModel.userCancelledRegistration()
navController.popBackStack(R.id.login_fragment, false)
})
}
}
class ChooseUserPasswordFragment : Fragment() {
private val loginViewModel: LoginViewModel by activityViewModels()
private val registrationViewModel: RegistrationViewModel by activityViewModels()
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController()
...
// When the register button is clicked, collect the current values from
// the two edit texts and pass to the ViewModel to complete registration.
view.findViewById<Button>(R.id.register_button).setOnClickListener {
registrationViewModel.createAccountAndLogin(
usernameEditText.text.toString(),
passwordEditText.text.toString()
)
}
// RegistrationViewModel updates the registrationState to
// REGISTRATION_COMPLETED when ready, and for this example, the username
// is accessed as a read-only property from RegistrationViewModel and is
// used to directly authenticate with loginViewModel.
registrationViewModel.registrationState.observe(
viewLifecycleOwner, Observer { state ->
if (state == REGISTRATION_COMPLETED) {
// Here we authenticate with the token provided by the ViewModel
// then pop back to the profie_fragment, where the user authentication
// status will be tested and should be authenticated.
val authToken = registrationViewModel.token
loginViewModel.authenticate(authToken)
navController.popBackStack(R.id.profile_fragment, false)
}
}
)
// If the user presses back, cancel the user registration and pop back
// to the login fragment. Since this ViewModel is shared at the activity
// scope, its state must be reset so that it is in the initial state if
// the user comes back to register later.
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
registrationViewModel.userCancelledRegistration()
navController.popBackStack(R.id.login_fragment, false)
})
}
}
Java
public class EnterProfileDataFragment extends Fragment {
private RegistrationViewModel registrationViewModel;
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
registrationViewModel = new ViewModelProvider(requireActivity())
.get(RegistrationViewModel.class);
final NavController navController = findNavController(view);
...
// When the next button is clicked, collect the current values from the two edit texts
// and pass to the ViewModel to store.
view.findViewById(R.id.next_button).setOnClickListener(v -> {
String name = fullnameEditText.getText().toString();
String bio = bioEditText.getText().toString();
registrationViewModel.collectProfileData(name, bio);
});
// RegistrationViewModel updates the registrationState to
// COLLECT_USER_PASSWORD when ready to move to the choose username and
// password screen.
registrationViewModel.getRegistrationState().observe(getViewLifecycleOwner(), state -> {
if (state == COLLECT_USER_PASSWORD) {
navController.navigate(R.id.move_to_choose_user_password);
}
});
// If the user presses back, cancel the user registration and pop back
// to the login fragment. Since this ViewModel is shared at the activity
// scope, its state must be reset so that it is in the initial state if
// the user comes back to register later.
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(),
new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
registrationViewModel.userCancelledRegistration();
navController.popBackStack(R.id.login_fragment, false);
}
});
}
}
class ChooseUserPasswordFragment extends Fragment {
private LoginViewModel loginViewModel;
private RegistrationViewModel registrationViewModel;
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
ViewModelProvider provider = new ViewModelProvider(requireActivity());
registrationViewModel = provider.get(RegistrationViewModel.class);
loginViewModel = provider.get(LoginViewModel.class);
final NavController navController = findNavController(view);
...
// When the register button is clicked, collect the current values from
// the two edit texts and pass to the ViewModel to complete registration.
view.findViewById(R.id.register_button).setOnClickListener(v ->
registrationViewModel.createAccountAndLogin(
usernameEditText.getText().toString(),
passwordEditText.getText().toString()
)
);
// RegistrationViewModel updates the registrationState to
// REGISTRATION_COMPLETED when ready, and for this example, the username
// is accessed as a read-only property from RegistrationViewModel and is
// used to directly authenticate with loginViewModel.
registrationViewModel.getRegistrationState().observe(
getViewLifecycleOwner(), state -> {
if (state == REGISTRATION_COMPLETED) {
// Here we authenticate with the token provided by the ViewModel
// then pop back to the profie_fragment, where the user authentication
// status will be tested and should be authenticated.
String authToken = registrationViewModel.getAuthToken();
loginViewModel.authenticate(authToken);
navController.popBackStack(R.id.profile_fragment, false);
}
}
);
// If the user presses back, cancel the user registration and pop back
// to the login fragment. Since this ViewModel is shared at the activity
// scope, its state must be reset so that it will be in the initial
// state if the user comes back to register later.
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(),
new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
registrationViewModel.userCancelledRegistration();
navController.popBackStack(R.id.login_fragment, false);
}
});
}
}
Keeping this FTUE flow separated into its own graph makes it easy to change the
sub-flow without affecting your main navigation flow. If you wanted to further
encapsulate the nested FTUE graph, you could also store it in a separate
navigation resource file and
include it
via an <include> element in your main navigation graph.

