条件导航

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

用户登录

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

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

图 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 = new ViewModelProvider(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 = 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() {
        ...
    }
    ...
}

如果用户在到达 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 = 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;
                }
            }
        });
    }
}

请注意,与身份验证相关的所有逻辑均保存在 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 = 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));
    }
    ...
}

并非所有导航操作都基于条件进行,但该模式对于基于条件的操作非常有用。您可以通过定义用户导航所基于的条件并在 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

双击编辑器中的 Nested 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 = 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);
                }
            });
    }
}

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