条件导航

在为应用设计导航时,您可能需要基于条件逻辑将用户转到某一个目的地而非另一个。例如,用户可能会跟随深层链接前往一个需要用户登录的目的地,或者您可能会在游戏中针对玩家的输赢提供不同的目的地。

用户登录

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

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

登录流程的处理独立于应用的主导航流程。
图 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 包含一个按钮,用户可以点击该按钮以查看其个人资料。如果用户想看到个人资料屏幕,必须先进行身份验证。此互动使用两个单独的 fragment 进行建模,但依赖于共享用户状态。上述两个 fragment 均不负责保存此状态信息,状态信息更适合保存在共享的 UserViewModel 中。在 fragment 之间共享此 ViewModel 的方法是将其范围限定为实现 ViewModelStoreOwner 的 activity。在以下示例中,由于 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 会使用上一个 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 会存储用户数据。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 中。这一点非常重要,因为 LoginFragmentProfileFragment 均不负责确定用户的身份验证方式。通过将逻辑封装在 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 会显示欢迎辞。

此处使用的检查结果的方法可让您区分两种不同的情况:

  • 在最初的情况下,用户未登录,系统应要求其登录。
  • 用户未登录,是因为他们选择不登录(结果为 false)。

通过区分这些用例,您可以避免反复要求用户登录。用于处理失败情况的业务逻辑由您来定,可能包括显示一个叠加层来解释为什么用户需要登录,完成整个 activity,或者将用户重定向到不需要登录的目的地,就像前面代码示例中的情况一样。