条件付きナビゲーション

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

ユーザー ログイン

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

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

アプリのメイン ナビゲーション フローとは独立して処理されるログインフロー
図 1. アプリのメイン ナビゲーション フローとは独立して処理されるログインフロー

認証を行うには、アプリは login_fragment に移動する必要があります。ユーザーはそこで、認証に使用するユーザー名とパスワードを入力できます。認証に成功した場合、ユーザーは profile_fragment 画面に戻ります。認証に成功しなかった場合は、Snackbar を通じて、認証情報が無効であることがユーザーに通知されます。ユーザーがログインせずにプロフィール画面に戻ると、main_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 には、ユーザーがプロフィールを表示する際にタップするボタンが含まれています。ユーザーがプロフィール画面を表示するには、その前に認証を行う必要があります。このインタラクションは、2 つの独立したフラグメントを使用してモデル化されますが、その方法は共有ユーザーの状態によって異なります。この状態情報を保持するのは、2 つのフラグメントのいずれでもなく、共有 UserViewModel が適切に保持します。ViewModel は、ViewModelStoreOwner を実装するアクティビティにスコープ設定することで、フラグメント間で共有されます。次の例の場合は、MainActivityProfileFragment をホストしているため、requireActivity()MainActivity に解決されます。

Kotlin

class ProfileFragment : Fragment() {
    private val userViewModel: UserViewModel by activityViewModels()
    ...
}

Java

public class ProfileFragment extends Fragment {
    private UserViewModel userViewModel;
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class);
        ...
    }
    ...
}

UserViewModel のユーザーデータは LiveData を通じてエクスポーズされます。そのため、ナビゲーション先を決定するには、このデータを検出する必要があります。ProfileFragment に移動する際、ユーザーデータが存在する場合は、アプリはウェルカム メッセージを表示します。ユーザーデータが null の場合は LoginFragment に移動します(プロフィールを表示するには、ユーザーの認証が必要です)。次の例に示すように、ProfileFragment で決定ロジックを定義します。

Kotlin

class ProfileFragment : Fragment() {
    private val userViewModel: UserViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val navController = findNavController()
        userViewModel.user.observe(viewLifecycleOwner, Observer { user ->
            if (user != null) {
                showWelcomeMessage()
            } else {
                navController.navigate(R.id.login_fragment)
            }
        })
    }

    private fun showWelcomeMessage() {
        ...
    }
}

Java

public class ProfileFragment extends Fragment {
    private UserViewModel userViewModel;

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class);
        final NavController navController = Navigation.findNavController(view);
        userViewModel.user.observe(getViewLifecycleOwner(), (Observer<User>) user -> {
            if (user != null) {
                showWelcomeMessage();
            } else {
                navController.navigate(R.id.login_fragment);
            }
        });
    }

    private void showWelcomeMessage() {
        ...
    }
}

ユーザーデータが ProfileFragment に到達した時点で null の場合、LoginFragment にリダイレクトされます。

NavController.getPreviousBackStackEntry() を使用して、前のデスティネーションの NavBackStackEntry を取得できます。これにより、デスティネーションの NavController 固有の状態がカプセル化されます。LoginFragment は、前の NavBackStackEntrySavedStateHandle を使用して、ユーザーがログインに成功したかどうかを示す初期値を設定します。この状態は、ユーザーがシステムの [戻る] ボタンを即座に押したときに返されます。SavedStateHandle を使用してこの状態を設定すると、プロセスが終了しても状態が維持されます。

Kotlin

class LoginFragment : Fragment() {
    companion object {
        const val LOGIN_SUCCESSFUL: String = "LOGIN_SUCCESSFUL"
    }

    private val userViewModel: UserViewModel by activityViewModels()
    private lateinit var savedStateHandle: SavedStateHandle

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        savedStateHandle = findNavController().previousBackStackEntry!!.savedStateHandle
        savedStateHandle.set(LOGIN_SUCCESSFUL, false)
    }
}

Java

public class LoginFragment extends Fragment {
    public static String LOGIN_SUCCESSFUL = "LOGIN_SUCCESSFUL"

    private UserViewModel userViewModel;
    private SavedStateHandle savedStateHandle;

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class);

        savedStateHandle = Navigation.findNavController(view)
                .getPreviousBackStackEntry()
                .getSavedStateHandle();
        savedStateHandle.set(LOGIN_SUCCESSFUL, false);
    }
}

ユーザーがユーザー名とパスワードを入力すると、UserViewModel に渡されて認証が行われます。認証が成功すると、UserViewModel はユーザーデータを保存します。次に、LoginFragmentSavedStateHandleLOGIN_SUCCESSFUL 値を更新し、バックスタックからポップします。

Kotlin

class LoginFragment : Fragment() {
    companion object {
        const val LOGIN_SUCCESSFUL: String = "LOGIN_SUCCESSFUL"
    }

    private val userViewModel: UserViewModel by activityViewModels()
    private lateinit var savedStateHandle: SavedStateHandle

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        savedStateHandle = findNavController().previousBackStackEntry!!.savedStateHandle
        savedStateHandle.set(LOGIN_SUCCESSFUL, false)

        val usernameEditText = view.findViewById(R.id.username_edit_text)
        val passwordEditText = view.findViewById(R.id.password_edit_text)
        val loginButton = view.findViewById(R.id.login_button)

        loginButton.setOnClickListener {
            val username = usernameEditText.text.toString()
            val password = passwordEditText.text.toString()
            login(username, password)
        }
    }

    fun login(username: String, password: String) {
        userViewModel.login(username, password).observe(viewLifecycleOwner, Observer { result ->
            if (result.success) {
                savedStateHandle.set(LOGIN_SUCCESSFUL, true)
                findNavController().popBackStack()
            } else {
                showErrorMessage()
            }
        })
    }

    fun showErrorMessage() {
        // Display a snackbar error message
    }
}

Java

public class LoginFragment extends Fragment {
    public static String LOGIN_SUCCESSFUL = "LOGIN_SUCCESSFUL"

    private UserViewModel userViewModel;
    private SavedStateHandle savedStateHandle;

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class);

        savedStateHandle = Navigation.findNavController(view)
                .getPreviousBackStackEntry()
                .getSavedStateHandle();
        savedStateHandle.set(LOGIN_SUCCESSFUL, false);

        EditText usernameEditText = view.findViewById(R.id.username_edit_text);
        EditText passwordEditText = view.findViewById(R.id.password_edit_text);
        Button loginButton = view.findViewById(R.id.login_button);

        loginButton.setOnClickListener(v -> {
            String username = usernameEditText.getText().toString();
            String password = passwordEditText.getText().toString();
            login(username, password);
        });
    }

    private void login(String username, String password) {
        userViewModel.login(username, password).observe(viewLifecycleOwner, (Observer<LoginResult>) result -> {
            if (result.success) {
                savedStateHandle.set(LOGIN_SUCCESSFUL, true);
                NavHostFragment.findNavController(this).popBackStack();
            } else {
                showErrorMessage();
            }
        });
    }

    private void showErrorMessage() {
        // Display a snackbar error message
    }
}

認証に関するロジックはすべて、UserViewModel 内で保持されます。ユーザーの認証方法を決定するのは、LoginFragment でも ProfileFragment でもないことから、この点は重要です。ロジックを ViewModel 内にカプセル化することで、共有が容易になるだけでなく、テストも容易になります。ナビゲーション ロジックが複雑な場合は特に、テストを通じてロジックを検証してください。テスト可能なコンポーネントを中心にアプリのアーキテクチャを構築する方法については、アプリのアーキテクチャ ガイドをご覧ください。

ProfileFragment では、SavedStateHandle に保存された LOGIN_SUCCESSFUL の値を onCreate() メソッド内で監視できます。ユーザーが ProfileFragment に戻ると、LOGIN_SUCCESSFUL 値が確認されます。値が false の場合、ユーザーは MainFragment にリダイレクトされます。

Kotlin

class ProfileFragment : Fragment() {
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val navController = findNavController()

        val currentBackStackEntry = navController.currentBackStackEntry!!
        val savedStateHandle = currentBackStackEntry.savedStateHandle
        savedStateHandle.getLiveData<Boolean>(LoginFragment.LOGIN_SUCCESSFUL)
                .observe(currentBackStackEntry, Observer { success ->
                    if (!success) {
                        val startDestination = navController.graph.startDestination
                        val navOptions = NavOptions.Builder()
                                .setPopUpTo(startDestination, true)
                                .build()
                        navController.navigate(startDestination, null, navOptions)
                    }
                })
    }

    ...
}

Java

public class ProfileFragment extends Fragment {
    ...

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        NavController navController = NavHostFragment.findNavController(this);

        NavBackStackEntry navBackStackEntry = navController.getCurrentBackStackEntry();
        SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle();
        savedStateHandle.getLiveData(LoginFragment.LOGIN_SUCCESSFUL)
                .observe(navBackStackEntry, (Observer<Boolean>) success -> {
                    if (!success) {
                        int startDestination = navController.getGraph().getStartDestination();
                        NavOptions navOptions = new NavOptions.Builder()
                                .setPopUpTo(startDestination, true)
                                .build();
                        navController.navigate(startDestination, null, navOptions);
                    }
                });
    }

    ...
}

ユーザーがログインに成功すると、ProfileFragment によってウェルカム メッセージが表示されます。

ここで使用される結果の確認手法により、次の 2 つのケースを区別できます。

  • ユーザーがログインしていない場合、ログインを求める必要のある最初のケース。
  • ユーザーがログインしないことを選択したので、ログインしていないケース(false の結果)。

このようなユースケースを区別することで、ユーザーに繰り返しログインを求める必要がなくなります。障害のケースを処理するビジネス ロジックはデベロッパーに委ねられており、ユーザー ログインが必要な理由を説明するオーバーレイを表示して、アクティビティ全体を終了するか、ログインを必要としないデスティネーションにユーザーをリダイレクトできます(前述のコードの例の場合と同じです)。