6 月 3 日の「#Android11: The Beta Launch Show」にぜひご参加ください。

条件付きナビゲーション

アプリのナビゲーションを設計する場合、条件付きロジックに基づいてデスティネーション間を移動するように設定できます。たとえば、一部のデスティネーションでユーザーのログインを必須にしたり、ゲーム内でプレーヤーの勝敗に応じてデスティネーションを変えたりすることができます。

ユーザー ログイン

ここでは、認証が必要なプロフィール画面にユーザーが移動するケースについて説明します。このアクションは認証を必要とするため、ユーザーがまだ認証を行っていない場合は、ログイン画面にリダイレクトされます。

この場合のナビゲーション グラフは次のようになります。

図 1: アプリのメイン ナビゲーション フローとは別個に処理されるログインフロー

認証を行うには、アプリは login_fragment に移動する必要があります。ユーザーはそこで、認証に使用するユーザー名とパスワードを入力します。認証に成功した場合、ユーザーは profile_fragment 画面に戻ります。認証に成功しなかった場合、Snackbar を通じて、認証情報が無効であることがユーザーに通知されます。

このアプリ内のデスティネーションは、1 つのアクティビティによってホストされている複数のフラグメントを使用して表現されます。

このアプリのナビゲーション グラフは次のとおりです。

<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 には、ユーザーがプロフィールを表示する際にタップするボタンが含まれています。ユーザーがプロフィール画面を表示するには、その前に認証を行う必要があります。このインタラクションは、2 つの独立したフラグメントを使用してモデル化されます。インタラクションの結果は、共有される状態情報(ユーザーの認証が済んでいるかどうか、認証が済んでいる場合は認証済みユーザー名)によって決定されます。ただし、この状態情報を保持するのは、2 つのフラグメントのいずれでもなく、共有 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 を実装するアクティビティに設定することで、フラグメント間でデータを共有できます。次の例の場合、MainActivityProfileFragment をホストしているため、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 を通じて、信頼できる情報の共有ソースを提供します。

ファースト タイム ユーザー エクスペリエンス

「ファースト タイム ユーザー エクスペリエンス(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 を持つ小さな [Nested Graph] として表示されます。図 3 をご覧ください。

図 3: ネストされた registration_graph が表示されるようになったナビゲーション グラフ

エディタ内で [Nested Graph] をダブルクリックすると、登録グラフの詳細が表示されます。図 4 のように、2 画面の登録フローが表示されます。1 つ目の画面は、ユーザーの氏名と経歴に関する情報を収集します。2 つ目の画面は、対象となるユーザー名とパスワードを収集します。メイン ナビゲーション グラフに戻るには、[Destinations] パネルで [← Root] をクリックします。

図 4: 登録フローを示すネストグラフ

このネスト ナビゲーション グラフの開始デスティネーションは、[Enter Profile Data] 画面です。ユーザーがプロフィール データを入力して [NEXT] ボタンをタップすると、アプリは [Create Login Credentials] 画面に移動します。ユーザーがユーザー名とパスワードの作成を完了すると、[REGISTER + LOGIN] をタップできるようになり、プロフィール画面に直接移動します。

前の例と同様、ViewModel を使用して、登録フラグメント間で情報を共有します。

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

    }
    

各登録画面のフラグメントから、この 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> 要素を通じてメイン ナビゲーション グラフ内に組み込むこともできます。