アプリのナビゲーションを設計する場合、条件付きロジックに基づいてデスティネーション間を移動するように設定できます。たとえば、ユーザーにログインが要求されるデスティネーションへのディープリンクを設定したり、プレーヤーの勝敗に応じてゲーム内にさまざまなデスティネーションを設定したりできます。
ユーザー ログイン
この例では、認証が必要なプロフィール画面にユーザーが移動するケースについて説明します。このアクションは認証を必要とするため、ユーザーがまだ認証を行っていない場合は、ログイン画面にリダイレクトされます。
この場合のナビゲーション グラフは次のようになります。
認証を行うには、アプリは 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
を実装するアクティビティにスコープ設定することで、フラグメント間で共有されます。次の例の場合は、MainActivity
が ProfileFragment
をホストしているため、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
は、前の NavBackStackEntry
の SavedStateHandle
を使用して、ユーザーがログインに成功したかどうかを示す初期値を設定します。この状態は、ユーザーがシステムの [戻る] ボタンを即座に押したときに返されます。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
はユーザーデータを保存します。次に、LoginFragment
が SavedStateHandle
の LOGIN_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
の結果)。
このようなユースケースを区別することで、ユーザーに繰り返しログインを求める必要がなくなります。障害のケースを処理するビジネス ロジックはデベロッパーに委ねられており、ユーザー ログインが必要な理由を説明するオーバーレイを表示して、アクティビティ全体を終了するか、ログインを必要としないデスティネーションにユーザーをリダイレクトできます(前述のコードの例の場合と同じです)。