O Google tem o compromisso de promover a igualdade racial para as comunidades negras. Saiba como.

Navegação condicional

Ao projetar a navegação para o app, você pode precisar navegar de um destino para outro seguindo a lógica condicional. Por exemplo, você pode ter alguns destinos que exigem que o usuário faça login, ou pode haver destinos diferentes em um jogo para quando o jogador ganha ou perde.

Login do usuário

Neste exemplo, um usuário tenta navegar para uma tela de perfil que requer autenticação. Como essa ação requer autenticação, o usuário precisa ser redirecionado para uma tela de login se ainda não estiver autenticado.

O gráfico de navegação para este exemplo pode ser parecido com este:

Figura 1: um fluxo de login é tratado independentemente do fluxo de navegação principal do app.

Para autenticar, o app precisa navegar até o login_fragment, em que o usuário pode inserir um nome de usuário e uma senha. Se for aceito, o usuário será enviado de volta para a tela profile_fragment. Se não for aceito, o usuário será informado de que as credenciais são inválidas usando Snackbar.

Os destinos neste app são representados usando fragmentos hospedados por uma única atividade.

Veja o gráfico de navegação deste 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 contém um botão no qual o usuário pode clicar se quiser visualizar o próprio perfil. Se o usuário quiser ver a tela do perfil, primeiro ele precisa ser autenticado. Essa interação é modelada usando dois fragmentos separados, mas isso depende de um estado compartilhado: se o usuário está autenticado e, em caso afirmativo, o nome de usuário autenticado. Observe que essas informações de estado não são de responsabilidade de um desses dois fragmentos e são mantidas mais apropriadamente em um ViewModel compartilhado, como mostrado no exemplo a seguir:

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

O escopo de um ViewModel é definido como um ViewModelStoreOwner. Você pode compartilhar dados entre os fragmentos, basta definir o escopo de um ViewModel como a atividade, que implementa ViewModelStoreOwner. No exemplo a seguir, requireActivity() resolve para MainActivity porque MainActivity hospeda 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);
        ...
    }
    ...
}

O estado de autenticação do usuário é representado como uma classe enum em LoginViewModel e exposta por LiveData. Portanto, para decidir aonde navegar, você precisa observar esse estado. Ao navegar para ProfileFragment, o app mostrará uma mensagem de boas-vindas se o usuário estiver autenticado. Se não estiver autenticado, você navegará para LoginFragment, já que o usuário precisará fazer a autenticação antes de ver o próprio perfil. É preciso definir a lógica de decisão no ViewModel, como mostrado no exemplo a seguir:

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

Se o usuário não estiver autenticado quando chegar ao ProfileFragment, ele navegará para o LoginFragment. Uma vez lá, ele poderá digitar um nome de usuário e uma senha, que serão então passados para LoginViewModel.

Se a autenticação for bem-sucedida, o ViewModel definirá o estado de autenticação como AUTHENTICATED. Isso faz com que LoginFragment seja removido da pilha de retorno, levando o usuário de volta ao ProfileFragment. Se a autenticação falhar devido a credenciais inválidas, o estado será definido como INVALID_AUTHENTICATION, e o usuário receberá um Snackbar no LoginFragment. Por fim, se ele pressionar o botão "Voltar", o estado será definido como UNAUTHENTICATED e a pilha será retornada para 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;
                }
            }
        });
    }
}

Observe que toda a lógica relacionada à autenticação será mantida em LoginViewModel. Isso é importante, já que não é responsabilidade de LoginFragment ou ProfileFragment determinar como os usuários são autenticados. O encapsulamento da lógica em um ViewModel facilita não só o compartilhamento, mas também os testes. Se a lógica de navegação for complexa, verifique-a especialmente por meio de testes. Consulte o Guia para a arquitetura do app para mais informações sobre como estruturar a arquitetura do app com componentes testáveis.

Quando o usuário retorna para ProfileFragment, o estado de autenticação é verificado novamente. Se ele for autenticado, o app exibirá uma mensagem de boas-vindas usando o nome de usuário autenticado, como mostrado no exemplo a seguir:

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

Nem todas as ações de navegação são baseadas em condições, mas esse padrão pode ser bastante útil para aquelas que são. Você determina como um usuário navega pelo app definindo as condições pelas quais ele navega e fornecendo uma fonte compartilhada de verdade em um ViewModel para comunicação entre fragmentos.

Primeira experiência do usuário

Uma primeira experiência do usuário (FTUE, na sigla em inglês) é um fluxo específico que os usuários veem apenas ao iniciar o app pela primeira vez. Em vez de fazer parte desse fluxo no gráfico de navegação principal do app, mantenha esse fluxo como um gráfico de navegação aninhado separado.

Criando um exemplo de login como na seção anterior, você pode ter um cenário em que o usuário pode se registrar se não tiver um login, como mostrado com um botão REGISTER na figura:

Figura 2: a tela de login agora contém um botão REGISTER.

Quando o usuário clica no botão REGISTER, é levado a um fluxo de subnavegação específico para o registro. Após o registro, a pilha de retorno é encerrada, e o usuário é levado diretamente para a tela do perfil.

O gráfico do componente de navegação no exemplo abaixo foi atualizado para incluir um gráfico de navegação aninhado. Uma ação também foi adicionada ao login_fragment e pode ser acionada em resposta ao tocar em 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>

Representado visualmente no Navigation Editor, este gráfico aninhado aparece como um gráfico aninhado menor, com o código do registration_graph em cima, conforme mostrado na figura 3:

Figura 3: o gráfico de navegação agora mostra o registration_graph aninhado.

Clique duas vezes no gráfico aninhado no editor para revelar os detalhes do gráfico de registro. Na figura 4, é possível ver um fluxo de registro de duas telas. A primeira tela reúne o nome completo do usuário e as informações biográficas. A segunda tela captura o nome de usuário e a senha desejados. Para voltar ao gráfico de navegação principal, clique em ← Root no painel Destinations.

Figura 4: o gráfico aninhado mostra o fluxo de registro.

O destino inicial desse gráfico de navegação aninhado é a tela Enter Profile Data. Quando o usuário insere dados de perfil e clica no botão NEXT, o app navega para a tela Create Login Credentials. Depois de criar o nome de usuário e a senha, ele poderá clicar em REGISTER + LOGIN para ser levado diretamente para a tela do perfil.

Assim como no exemplo anterior, um ViewModel é usado para compartilhar informações entre os 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;
    }

}

O estado de registro deste ViewModel é observado nos fragmentos de cada tela de registro. O estado impulsiona o movimento para a próxima tela e é atualizado por RegistrationViewModel com base nas interações do usuário. Se "Voltar" for pressionado a qualquer momento, o processo de registro será cancelado e o usuário voltará para a tela de login:

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

Manter esse fluxo FTUE separado no seu próprio gráfico facilita a mudança do subfluxo sem afetar o fluxo de navegação principal. Se quiser encapsular ainda mais o gráfico aninhado de FTUE, você também poderá armazená-lo em um arquivo de recursos de navegação separado e incluí-lo em um elemento <include> no seu gráfico de navegação principal.