Únete a ⁠ #Android11: The Beta Launch Show el 3 de junio.

Navegación condicional

Cuando diseñas la navegación para tu app, es posible que desees navegar a un destino en lugar de otro según la lógica condicional. Por ejemplo, podrías tener algunos destinos que requieren que el usuario acceda o podrías tener diferentes destinos en un juego para cuando el jugador gana o pierde.

Acceso del usuario

En este ejemplo, un usuario intenta navegar a una pantalla de perfil que requiere autenticación. Debido a que esta acción requiere autenticación, el usuario debe ser redirigido a una pantalla de acceso si todavía no está autenticado.

El gráfico de navegación de este ejemplo podría verse de la siguiente manera:

Figura 1: Un flujo de acceso se maneja de manera independiente del flujo de navegación principal de la app

Para autenticar, tu app debe navegar a login_fragment, donde el usuario puede ingresar un nombre de usuario y una contraseña para autenticarse. Si se aceptan las credenciales, el usuario vuelve a la pantalla profile_fragment. Si no se aceptan, se informa al usuario que sus credenciales no son válidas mediante un Snackbar.

Los destinos de esta app se representan mediante fragmentos alojados por una sola actividad.

A continuación, se muestra el gráfico de navegación para esta 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 contiene un botón en el que el usuario puede hacer clic si desea ver su perfil. Si el usuario desea ver la pantalla de perfil, primero debe autenticarse. Esta interacción se modela con dos fragmentos separados, pero depende de un estado compartido: si el usuario está autenticado y, en ese caso, el nombre de usuario autenticado. Ten en cuenta que esta información de estado no es responsabilidad de ninguno de estos dos fragmentos y se mantiene más apropiadamente en un ViewModel compartido, como se muestra en el siguiente ejemplo:

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) {
            ...
        }
    }
    

Un ViewModel tiene un alcance de ViewModelStoreOwner. Puedes compartir datos entre los fragmentos al tener un alcance de ViewModel para la actividad, que implementa ViewModelStoreOwner. En el siguiente ejemplo, requireActivity() se resuelve en MainActivity porque MainActivity aloja 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 = ViewModelProviders.of(requireActivity()).get(LoginViewModel.class);
            ...
        }
        ...
    }
    

El estado de autenticación del usuario se representa como una clase enum en LoginViewModel y se expone mediante LiveData; por lo tanto, para decidir dónde navegar, deberías observar ese estado. Cuando navegas a ProfileFragment, la app muestra un mensaje de bienvenida si el usuario está autenticado. Si no está autenticado, navegarás a LoginFragment, ya que el usuario debe autenticarse antes de ver su perfil. Debes definir la lógica de decisión en tu ViewModel, como se muestra en el siguiente ejemplo:

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 = ViewModelProviders.of(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() {
            ...
        }
        ...
    }
    

Si el usuario no está autenticado cuando llega a ProfileFragment, navega hasta LoginFragment. Una vez allí, puede ingresar un nombre de usuario y una contraseña, que luego se pasa a LoginViewModel.

Si la autenticación se realiza correctamente, ViewModel establece el estado de autenticación en AUTHENTICATED. Esto hace que LoginFragment se quite de la pila de actividades, lo que lleva al usuario de regreso a ProfileFragment. Si la autenticación no es correcta porque las credenciales no son válidas, el estado se configura como INVALID_AUTHENTICATION y se le muestra Snackbar al usuario en LoginFragment. Por último, si se presiona el botón Atrás, el estado se establece en UNAUTHENTICATED y la pila vuelve a aparecer en 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 = ViewModelProviders.of(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;
                    }
                }
            });
        }
    }
    

Ten en cuenta que toda la lógica relacionada con la autenticación se mantiene dentro de LoginViewModel. Esto es importante, ya que no es responsabilidad de LoginFragment ni de ProfileFragment determinar cómo se autentican los usuarios. Encapsular tu lógica en un ViewModel hace que compartir y probarla sea más fácil. Si tu lógica de navegación es compleja, debes verificar especialmente esta lógica mediante pruebas. Consulta la Guía de arquitectura de apps para obtener más información sobre cómo estructurar la arquitectura de tu app en torno a los componentes que se pueden probar.

Cuando el usuario vuelve a ProfileFragment, su estado de autenticación se vuelve a comprobar. Si ahora está autenticado, la app muestra un mensaje de bienvenida con el nombre de usuario autenticado, como se muestra en el siguiente ejemplo:

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 = ViewModelProviders.of(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));
        }
        ...
    }
    

No todas las acciones de navegación se basan en las condiciones, pero este patrón puede ser muy útil para las que sí lo hacen. A fin de determinar cómo navega un usuario por tu app, define las condiciones por las que navega y proporciona una fuente de confianza compartida en un ViewModel para la comunicación entre fragmentos.

Primera experiencia del usuario

Una primera experiencia del usuario (FTUE) es un flujo específico que los usuarios ven solo cuando inician tu app por primera vez. En lugar de hacer que este flujo forme parte del gráfico de navegación principal de tu app, deberían mantenerlo como un gráfico de navegación anidado separado.

Partiendo del ejemplo de acceso de la sección anterior, es posible que tengas un escenario en el que el usuario tiene la posibilidad de registrarse si no tiene acceso, como se muestra con un botón REGISTER en la siguiente figura:

Figura 2: La pantalla de acceso ahora contiene un botón REGISTER

Cuando el usuario hace clic en el botón REGISTER , se lo dirige a un flujo de subnavegación específico para el registro. Después de registrarse, se abre la pila de actividades y se lleva al usuario directamente a la pantalla de perfil.

El gráfico de navegación del siguiente ejemplo, se actualizó para incluir un gráfico de navegación anidado. También se agregó una acción al login_fragment y puede activarse en respuesta al presionar 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>
    

Este gráfico anidado, representado visualmente en el editor de Navigation, aparece como un gráfico anidado más pequeño con el ID registration_graph en la parte superior, como se muestra en la figura 3:

Figura 3: El gráfico de navegación ahora muestra el registration_graph anidado

Haz doble clic en el gráfico anidado del editor para revelar los detalles del gráfico de registro. En la figura 4, puedes ver un flujo de registro de dos pantallas. La primera pantalla recopila el nombre completo del usuario y la información biográfica. La segunda pantalla captura su nombre de usuario y contraseña deseados. Para volver al gráfico de navegación principal, haz clic en ← Root en el panel Destinations.

Figura 4: El gráfico anidado muestra el flujo de registro

El destino de inicio de este gráfico de navegación anidado es la pantalla Enter Profile Data. Una vez que el usuario ingresa los datos del perfil y hace clic en el botón NEXT, la app navega a la pantalla Create Login Credentials. Cuando termine de crear su nombre de usuario y contraseña, puede hacer clic en REGISTER + LOGIN para acceder directamente a la pantalla de perfil.

Al igual que en el ejemplo anterior, se utiliza un ViewModel para compartir información entre los fragmentos de registro:

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;
        }

    }
    

El estado de registro de este ViewModel se observa a partir de los fragmentos de cada pantalla de registro. El estado conduce a la siguiente pantalla y se actualiza en RegistrationViewModel según las interacciones del usuario. Cuando se presiona la tecla de retroceso, en cualquier momento, se cancela el proceso de registro y se vuelve a mostrar al usuario en la pantalla de acceso:

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 = ViewModelProviders
                    .of(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 = ViewModelProviders.of(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);
                    }
                });
        }
    }
    

Mantener este flujo FTUE separado en su propio gráfico facilita el cambio del subflujo sin afectar tu flujo de navegación principal. Si quieres encapsular aún más el gráfico FTUE anidado, también puedes almacenarlo en un archivo de recursos de navegación separado y agregarlo mediante un elemento <include> en tu gráfico de navegación principal.