第二个 Android 11 开发者预览版现已推出,快来测试并分享您的反馈吧

条件导航

在为应用设计导航时,您可能需要基于条件逻辑将用户转到某一个目的地而非另一个。例如,您可能具有一些需要用户登录的目的地,或者您可能在游戏中针对获胜或失败的玩家提供了不同的目的地。

用户登录

在此示例中,用户尝试转到需要进行身份验证的个人资料屏幕。由于此操作需要进行身份验证,因此系统应将用户重定向到登录屏幕(如果用户尚未通过身份验证)。

此示例的导航图大致如下所示:

图 1:登录流程的处理独立于应用的主导航流程。

要进行身份验证,应用必须转到 login_fragment,用户可以在此处输入用户名和密码来进行身份验证。如果被接受,系统会将用户重定向回 profile_fragment 屏幕。如果未被接受,系统会使用 Snackbar 通知用户其凭据无效。

该应用中的目的地使用由单个 Activity 托管的 Fragment 来表示。

以下是该应用的导航图:

<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 包含一个按钮,如果用户想查看其个人资料,可以点击此按钮。如果用户想看到个人资料屏幕,必须先进行身份验证。此互动使用两个单独的 Fragment 进行建模,但具体取决于共享状态 - 用户是否已进行身份验证,如果是,通过身份验证的用户名是什么。请注意,上述两个 Fragment 均不负责保存此状态信息,它更适合保存在共享的 ViewModel 中,如以下示例所示:

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

ViewModel 的范围限定为 ViewModelStoreOwner。您可以通过将 ViewModel 的范围限定为实现 ViewModelStoreOwner 的 Activity,在 Fragment 之间共享数据。在以下示例中,由于 MainActivity 托管 ProfileFragment,因此 requireActivity() 解析为 MainActivity

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

用户的身份验证状态在 LoginViewModel 中表示为枚举类,并通过 LiveData 公开,因此,为了确定要导航到的位置,您应该观察该状态。转到 ProfileFragment 后,如果用户已通过身份验证,应用会显示欢迎辞。如果用户未通过身份验证,您应将其转到 LoginFragment,因为用户需要先进行身份验证,然后才能查看其个人资料。您需要在 ViewModel 中定义确定逻辑,如以下示例所示:

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

如果用户在到达 ProfileFragment 时未通过身份验证,则系统会将他们转到 LoginFragment。在那里,用户能够输入用户名和密码,这些信息随后会传递到 LoginViewModel

如果成功通过身份验证,ViewModel 会将身份验证状态设置为 AUTHENTICATED。这会使 LoginFragment 从返回堆栈中退出,并将用户带回 ProfileFragment。如果身份验证由于凭据无效而失败,则系统会将状态设置为 INVALID_AUTHENTICATION,并且会在 LoginFragment 中向用户显示 Snackbar。最后,如果用户按下返回按钮,则系统会将状态设置为 UNAUTHENTICATED,并使堆栈返回到 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;
                    }
                }
            });
        }
    }
    

请注意,与身份验证相关的所有逻辑均保存在 LoginViewModel 中。这一点非常重要,因为 LoginFragmentProfileFragment 均不负责确定用户的身份验证方式。通过将逻辑封装在 ViewModel 中,您可以更轻松地进行共享和测试。如果您的导航逻辑非常复杂,则您应特别注意通过测试来验证该逻辑。如需详细了解如何围绕可测试的组件构建应用架构,请参阅应用架构指南

当用户返回到 ProfileFragment 时,系统会再次检查他们的身份验证状态。如果用户现已通过身份验证,则应用会使用通过身份验证的用户名显示欢迎辞,如以下示例所示:

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

并非所有导航操作都基于条件进行,但该模式对于基于条件的操作非常有用。您可以通过定义用户导航所基于的条件并在 ViewModel 中提供共享可信来源(用于 Fragment 之间的通信),确定用户如何在您的应用中进行导航。

首次用户体验

首次用户体验 (FTUE) 是用户只有在首次启动应用时才会看到的特定流程。您应该将此流程作为一个单独的嵌套导航图,而不是使其成为应用的主导航图的一部分。

就上一部分中的登录示例而言,您可能具有以下情况:用户没有登录时,有机会进行注册,如图中的 REGISTER 按钮所示:

图 2:登录屏幕现在包含一个 REGISTER 按钮。

当用户点击 REGISTER 按钮时,系统会将其转到专用于注册的子导航流程。用户注册后,系统会弹出返回堆栈,并直接将用户转至个人资料屏幕。

以下示例中的导航图已更新,包含一个嵌套导航图。此外,login_fragment 中还添加了一个操作,系统可以触发该操作来响应对 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>
    

该嵌套图表在 Navigation Editor 中以直观方式呈现,显示为一个较小的最上层具有 registration_graph ID 的嵌套图表,如图 3 所示:

图 3:导航图现在显示嵌套的 registration_graph

双击编辑器中的嵌套图表,以显示注册图表的详细信息。在图 4 中,您可以看到一个双屏注册流程。第一个屏幕会收集用户的全名和最基本的信息。第二个屏幕会捕获用户想要使用的用户名和密码。要返回主导航图,请点击 Destinations 窗格中的 ← Root

图 4:该嵌套图表显示注册流程。

该嵌套导航图的起始目的地为 Enter Profile Data 屏幕。用户输入个人资料数据并点击 NEXT 按钮后,应用便会将其转到 Create Login Credentials 屏幕。用户名和密码创建完成后,他们可以点击 REGISTER + LOGIN 直接进入个人资料屏幕。

与之前的示例一样,ViewModel 用于在注册 Fragment 之间共享信息:

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

    }
    

您可以从每个注册屏幕的 Fragment 中观察该 ViewModel 的注册状态。该状态可推动进入下一个屏幕,并由 RegistrationViewModel 基于用户互动情况进行更新。按返回按钮可随时取消注册流程,并使用户返回到登录屏幕:

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

通过将该 FTUE 流程分离到其自己的图表中,您可以轻松更改子流程,而不会影响主导航流程。如果您想进一步封装嵌套的 FTUE 图表,还可以将其存储在单独的导航资源文件中,并通过 <include> 元素将其包含在主导航图中。